/*
* 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.config.profile;
import com.novell.ldapchai.ChaiPasswordPolicy;
import com.novell.ldapchai.ChaiPasswordRule;
import com.novell.ldapchai.util.DefaultChaiPasswordPolicy;
import com.novell.ldapchai.util.PasswordRuleHelper;
import com.novell.ldapchai.util.StringHelper;
import password.pwm.config.UserPermission;
import password.pwm.config.option.ADPolicyComplexity;
import password.pwm.health.HealthMessage;
import password.pwm.health.HealthRecord;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.StringUtil;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.macro.MacroMachine;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* @author Jason D. Rivard
*/
public class PwmPasswordPolicy implements Profile,Serializable {
// ------------------------------ FIELDS ------------------------------
private static final PwmLogger LOGGER = PwmLogger.forClass(PwmPasswordPolicy.class);
private static final PwmPasswordPolicy DEFAULT_POLICY;
private final Map<String, String> policyMap = new HashMap<>();
private final transient ChaiPasswordPolicy chaiPasswordPolicy;
private String profileID;
private List<UserPermission> userPermissions;
private String ruleText;
public static PwmPasswordPolicy createPwmPasswordPolicy(
final Map<String, String> policyMap,
final ChaiPasswordPolicy chaiPasswordPolicy
) {
return new PwmPasswordPolicy(policyMap, chaiPasswordPolicy);
}
public String getIdentifier() {
return profileID;
}
public String getDisplayName(final Locale locale) {
return getIdentifier();
}
// -------------------------- STATIC METHODS --------------------------
static {
PwmPasswordPolicy newDefaultPolicy = null;
try {
final Map<String, String> defaultPolicyMap = new HashMap<>();
for (final PwmPasswordRule rule : PwmPasswordRule.values()) {
defaultPolicyMap.put(rule.getKey(), rule.getDefaultValue());
}
newDefaultPolicy = createPwmPasswordPolicy(defaultPolicyMap, null);
} catch (Throwable t) {
LOGGER.fatal("error initializing PwmPasswordPolicy class: " + t.getMessage(), t);
}
DEFAULT_POLICY = newDefaultPolicy;
}
public static PwmPasswordPolicy defaultPolicy() {
return DEFAULT_POLICY;
}
private PwmPasswordPolicy(
final Map<String, String> policyMap,
final ChaiPasswordPolicy chaiPasswordPolicy
) {
if (policyMap != null) {
this.policyMap.putAll(policyMap);
}
if (chaiPasswordPolicy != null) {
if (Boolean.parseBoolean(chaiPasswordPolicy.getValue(ChaiPasswordRule.ADComplexity))) {
this.policyMap.put(PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2003.toString());
} else if (Boolean.parseBoolean(chaiPasswordPolicy.getValue(ChaiPasswordRule.ADComplexity2008))) {
this.policyMap.put(PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2008.toString());
}
}
this.chaiPasswordPolicy = chaiPasswordPolicy;
}
@Override
public String toString() {
return "PwmPasswordPolicy" + ": " + JsonUtil.serialize(this);
}
public ChaiPasswordPolicy getChaiPasswordPolicy() {
return chaiPasswordPolicy;
}
public RuleHelper getRuleHelper() {
return new RuleHelper(this);
}
public String getValue(final PwmPasswordRule rule) {
return policyMap.get(rule.getKey());
}
public void setProfileID(final String profileID) {
this.profileID = profileID;
}
public List<UserPermission> getUserPermissions() {
return userPermissions;
}
public void setUserPermissions(final List<UserPermission> userPermissions) {
this.userPermissions = userPermissions;
}
public String getRuleText() {
return ruleText;
}
public void setRuleText(final String ruleText) {
this.ruleText = ruleText;
}
public PwmPasswordPolicy merge(final PwmPasswordPolicy otherPolicy) {
if (otherPolicy == null) {
return this;
}
final Map<String, String> newPasswordPolicies = new HashMap<>();
for (final PwmPasswordRule rule : PwmPasswordRule.values()) {
final String ruleKey = rule.getKey();
if (this.policyMap.containsKey(ruleKey) || otherPolicy.policyMap.containsKey(ruleKey)) {
switch (rule) {
case DisallowedValues:
case DisallowedAttributes:
case RegExMatch:
case RegExNoMatch:
case CharGroupsValues:
final String seperator = (rule == PwmPasswordRule.RegExMatch || rule == PwmPasswordRule.RegExNoMatch) ? ";;;" : "\n";
final Set<String> combinedSet = new HashSet<>();
combinedSet.addAll(StringHelper.tokenizeString(this.policyMap.get(rule.getKey()), seperator));
combinedSet.addAll(StringHelper.tokenizeString(otherPolicy.policyMap.get(rule.getKey()), seperator));
newPasswordPolicies.put(ruleKey, StringHelper.stringCollectionToString(combinedSet, seperator));
break;
case ChangeMessage:
final String thisChangeMessage = getValue(PwmPasswordRule.ChangeMessage);
if (thisChangeMessage == null || thisChangeMessage.length() < 1) {
newPasswordPolicies.put(ruleKey, otherPolicy.getValue(PwmPasswordRule.ChangeMessage));
} else {
newPasswordPolicies.put(ruleKey, getValue(PwmPasswordRule.ChangeMessage));
}
break;
case ExpirationInterval:
final String expirationIntervalLocalValue = StringUtil.defaultString(policyMap.get(ruleKey), rule.getDefaultValue());
final String expirationIntervalOtherValue = StringUtil.defaultString(otherPolicy.policyMap.get(ruleKey), rule.getDefaultValue());
newPasswordPolicies.put(ruleKey, mergeMin(expirationIntervalLocalValue, expirationIntervalOtherValue));
break;
case MinimumLifetime:
final String minimumLifetimeLocalValue = StringUtil.defaultString(policyMap.get(ruleKey), rule.getDefaultValue());
final String minimumLifetimeOtherValue = StringUtil.defaultString(otherPolicy.policyMap.get(ruleKey), rule.getDefaultValue());
newPasswordPolicies.put(ruleKey, mergeMin(minimumLifetimeLocalValue, minimumLifetimeOtherValue));
break;
default:
final String localValueString = StringUtil.defaultString(policyMap.get(ruleKey), rule.getDefaultValue());
final String otherValueString = StringUtil.defaultString(otherPolicy.policyMap.get(ruleKey), rule.getDefaultValue());
switch (rule.getRuleType()) {
case MIN:
newPasswordPolicies.put(ruleKey, mergeMin(localValueString, otherValueString));
break;
case MAX:
newPasswordPolicies.put(ruleKey, mergeMax(localValueString, otherValueString));
break;
case BOOLEAN:
final boolean localValue = StringHelper.convertStrToBoolean(localValueString);
final boolean otherValue = StringHelper.convertStrToBoolean(otherValueString);
if (rule.isPositiveBooleanMerge()) {
newPasswordPolicies.put(ruleKey, String.valueOf(localValue || otherValue));
} else {
newPasswordPolicies.put(ruleKey, String.valueOf(localValue && otherValue));
}
break;
default:
//continue processing
break;
}
}
}
}
final ChaiPasswordPolicy backingPolicy = this.chaiPasswordPolicy != null ? chaiPasswordPolicy : otherPolicy.chaiPasswordPolicy;
final PwmPasswordPolicy returnPolicy = createPwmPasswordPolicy(newPasswordPolicies, backingPolicy);
final String newRuleText = (ruleText != null && !ruleText.isEmpty()) ? ruleText : otherPolicy.ruleText;
returnPolicy.setRuleText(newRuleText);
return returnPolicy;
}
protected static String mergeMin(final String value1, final String value2) {
final int iValue1 = StringHelper.convertStrToInt(value1, 0);
final int iValue2 = StringHelper.convertStrToInt(value2, 0);
// take the largest value
return iValue1 > iValue2 ? value1 : value2;
}
protected static String mergeMax(final String value1, final String value2) {
final int iValue1 = StringHelper.convertStrToInt(value1, 0);
final int iValue2 = StringHelper.convertStrToInt(value2, 0);
final String returnValue;
// if one of the values is zero, take the other one.
if (iValue1 == 0 || iValue2 == 0) {
returnValue = iValue1 > iValue2 ? value1 : value2;
// else take the smaller value
} else {
returnValue = iValue1 < iValue2 ? value1 : value2;
}
return returnValue;
}
public static PwmPasswordPolicy createPwmPasswordPolicy(final Map<String, String> policyMap) {
return createPwmPasswordPolicy(policyMap, null);
}
// -------------------------- INNER CLASSES --------------------------
public static class RuleHelper {
public enum Flag { KeepThresholds }
private final PwmPasswordPolicy passwordPolicy;
private final PasswordRuleHelper chaiRuleHelper;
public RuleHelper(final PwmPasswordPolicy passwordPolicy) {
this.passwordPolicy = passwordPolicy;
chaiRuleHelper = DefaultChaiPasswordPolicy.createDefaultChaiPasswordPolicy(passwordPolicy.policyMap).getRuleHelper();
}
public List<String> getDisallowedValues() {
return chaiRuleHelper.getDisallowedValues();
}
public List<String> getDisallowedAttributes(final Flag ... flags) {
final List<String> disallowedAttributes = chaiRuleHelper.getDisallowedAttributes();
if (JavaHelper.enumArrayContainsValue(flags, Flag.KeepThresholds)) {
return disallowedAttributes;
} else {
// Strip off any thresholds from attribute (specified as: "attributeName:N", where N is a numeric value).
final List<String> strippedDisallowedAttributes = new ArrayList<String>();
if (disallowedAttributes != null) {
for (final String disallowedAttribute : disallowedAttributes) {
if (disallowedAttribute != null) {
final int indexOfColon = disallowedAttribute.indexOf(':');
if (indexOfColon > 0) {
strippedDisallowedAttributes.add(disallowedAttribute.substring(0, indexOfColon));
} else {
strippedDisallowedAttributes.add(disallowedAttribute);
}
}
}
}
return strippedDisallowedAttributes;
}
}
public List<Pattern> getRegExMatch(final MacroMachine macroMachine) {
return readRegExSetting(PwmPasswordRule.RegExMatch, macroMachine);
}
public List<Pattern> getRegExNoMatch(final MacroMachine macroMachine) {
return readRegExSetting(PwmPasswordRule.RegExNoMatch, macroMachine);
}
public List<Pattern> getCharGroupValues() {
return readRegExSetting(PwmPasswordRule.CharGroupsValues, null);
}
public int readIntValue(final PwmPasswordRule rule) {
if (
(rule.getRuleType() != ChaiPasswordRule.RuleType.MIN) &&
(rule.getRuleType() != ChaiPasswordRule.RuleType.MAX) &&
(rule.getRuleType() != ChaiPasswordRule.RuleType.NUMERIC)
) {
throw new IllegalArgumentException("attempt to read non-numeric rule value as int for rule " + rule);
}
final String value = passwordPolicy.policyMap.get(rule.getKey());
final int defaultValue = StringHelper.convertStrToInt(rule.getDefaultValue(), 0);
return StringHelper.convertStrToInt(value, defaultValue);
}
public boolean readBooleanValue(final PwmPasswordRule rule) {
if (rule.getRuleType() != ChaiPasswordRule.RuleType.BOOLEAN) {
throw new IllegalArgumentException("attempt to read non-boolean rule value as boolean for rule " + rule);
}
final String value = passwordPolicy.policyMap.get(rule.getKey());
return StringHelper.convertStrToBoolean(value);
}
private List<Pattern> readRegExSetting(final PwmPasswordRule rule, final MacroMachine macroMachine) {
final String input = passwordPolicy.policyMap.get(rule.getKey());
return readRegExSetting(rule, macroMachine, input);
}
List<Pattern> readRegExSetting(final PwmPasswordRule rule, final MacroMachine macroMachine, final String input) {
if (input == null) {
return Collections.emptyList();
}
final String separator = (rule == PwmPasswordRule.RegExMatch || rule == PwmPasswordRule.RegExNoMatch) ? ";;;" : "\n";
final List<String> values = new ArrayList<>(StringHelper.tokenizeString(input, separator));
final List<Pattern> patterns = new ArrayList<>();
for (final String value : values) {
if (value != null && value.length() > 0) {
String valueToCompile = value;
if (macroMachine != null && readBooleanValue(PwmPasswordRule.AllowMacroInRegExSetting)) {
valueToCompile = macroMachine.expandMacros(value);
}
try {
final Pattern loopPattern = Pattern.compile(valueToCompile);
patterns.add(loopPattern);
} catch (PatternSyntaxException e) {
LOGGER.warn("reading password rule value '" + valueToCompile + "' for rule " + rule.getKey() + " is not a valid regular expression " + e.getMessage());
}
}
}
return patterns;
}
public String getChangeMessage() {
final String changeMessage = passwordPolicy.getValue(PwmPasswordRule.ChangeMessage);
return changeMessage == null ? "" : changeMessage;
}
public ADPolicyComplexity getADComplexityLevel() {
final String strLevel = passwordPolicy.getValue(PwmPasswordRule.ADComplexityLevel);
if (strLevel == null || strLevel.isEmpty()) {
return ADPolicyComplexity.NONE;
}
return ADPolicyComplexity.valueOf(strLevel);
}
}
public Map<String, String> getPolicyMap() {
return Collections.unmodifiableMap(policyMap);
}
@Override
public ProfileType profileType() {
throw new UnsupportedOperationException();
}
@Override
public List<UserPermission> getPermissionMatches() {
throw new UnsupportedOperationException();
}
public List<HealthRecord> health(final Locale locale) {
final RuleHelper ruleHelper = this.getRuleHelper();
final List<HealthRecord> returnList = new ArrayList<>();
final Map<PwmPasswordRule, PwmPasswordRule> rulePairs = new LinkedHashMap<>();
rulePairs.put(PwmPasswordRule.MinimumLength, PwmPasswordRule.MaximumLength);
rulePairs.put(PwmPasswordRule.MinimumLowerCase, PwmPasswordRule.MaximumLowerCase);
rulePairs.put(PwmPasswordRule.MinimumUpperCase, PwmPasswordRule.MaximumUpperCase);
rulePairs.put(PwmPasswordRule.MinimumNumeric, PwmPasswordRule.MaximumNumeric);
rulePairs.put(PwmPasswordRule.MinimumSpecial, PwmPasswordRule.MaximumSpecial);
rulePairs.put(PwmPasswordRule.MinimumAlpha, PwmPasswordRule.MaximumAlpha);
rulePairs.put(PwmPasswordRule.MinimumNonAlpha, PwmPasswordRule.MaximumNonAlpha);
rulePairs.put(PwmPasswordRule.MinimumUnique, PwmPasswordRule.MaximumUnique);
for (final PwmPasswordRule minRule : rulePairs.keySet()) {
final PwmPasswordRule maxRule = rulePairs.get(minRule);
final int minValue = ruleHelper.readIntValue(minRule);
final int maxValue = ruleHelper.readIntValue(maxRule);
if (maxValue > 0 && minValue > maxValue) {
final String detailMsg = minRule.getLabel(locale, null) + " (" + minValue + ")"
+ " > "
+ maxRule.getLabel(locale, null) + " (" + maxValue + ")";
returnList.add(HealthRecord.forMessage(HealthMessage.Config_PasswordPolicyProblem, profileID, detailMsg));
}
}
{
final int minValue = ruleHelper.readIntValue(PwmPasswordRule.CharGroupsMinMatch);
final List<Pattern> ruleGroups = ruleHelper.getCharGroupValues();
final int maxValue = ruleGroups == null ? 0 : ruleGroups.size();
if (maxValue > 0 && minValue > maxValue) {
final String detailMsg = PwmPasswordRule.CharGroupsValues.getLabel(locale, null) + " (" + minValue + ")"
+ " > "
+ PwmPasswordRule.CharGroupsMinMatch.getLabel(locale, null) + " (" + maxValue + ")";
returnList.add(HealthRecord.forMessage(HealthMessage.Config_PasswordPolicyProblem, profileID, detailMsg));
}
}
return Collections.unmodifiableList(returnList);
}
}