Browse Source

Merge remote-tracking branch 'origin/master'

Jason Rivard 9 years ago
parent
commit
56893438eb

+ 1 - 0
src/main/java/password/pwm/AppProperty.java

@@ -243,6 +243,7 @@ public enum AppProperty {
     URL_SHORTNER_URL_REGEX                          ("urlshortener.url.regex"),
     WORDLIST_BUILTIN_PATH                           ("wordlist.builtin.path"),
     WS_REST_CLIENT_PWRULE_HALTONERROR               ("ws.restClient.pwRule.haltOnError"),
+    ALLOW_MACRO_IN_REGEX_SETTING                    ("password.policy.allowMacroInRegexSetting"),
 
     ;
 

+ 4 - 1
src/main/java/password/pwm/config/Configuration.java

@@ -377,7 +377,7 @@ public class Configuration implements Serializable, SettingReader {
     {
         final Map<String, String> passwordPolicySettings = new LinkedHashMap<>();
         for (final PwmPasswordRule rule : PwmPasswordRule.values()) {
-            if (rule.getPwmSetting() != null) {
+            if (rule.getPwmSetting() != null || rule.getAppProperty() != null) {
                 final String value;
                 final PwmSetting pwmSetting = rule.getPwmSetting();
                 switch (rule) {
@@ -403,6 +403,9 @@ public class Configuration implements Serializable, SettingReader {
                                 ADPolicyComplexity.class
                         ).toString();
                         break;
+                    case AllowMacroInRegExSetting:
+                        value = readAppProperty(AppProperty.ALLOW_MACRO_IN_REGEX_SETTING);
+                        break;
                     default:
                         value = String.valueOf(
                                 storedConfiguration.readSetting(pwmSetting, profile).toNativeObject());

+ 55 - 23
src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java

@@ -22,11 +22,21 @@
 
 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 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;
+
+import org.apache.commons.lang3.StringUtils;
+
 import password.pwm.config.UserPermission;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.health.HealthMessage;
@@ -34,11 +44,13 @@ import password.pwm.health.HealthRecord;
 import password.pwm.util.Helper;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
 
-import java.io.Serializable;
-import java.util.*;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
+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;
 
 
 /**
@@ -159,6 +171,7 @@ public class PwmPasswordPolicy implements Profile,Serializable {
         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:
@@ -182,26 +195,33 @@ public class PwmPasswordPolicy implements Profile,Serializable {
                         break;
 
                     case ExpirationInterval:
-                        newPasswordPolicies.put(ruleKey, mergeMin(policyMap.get(ruleKey), otherPolicy.policyMap.get(ruleKey)));
+                        final String expirationIntervalLocalValue = StringUtils.defaultString(policyMap.get(ruleKey), rule.getDefaultValue());
+                        final String expirationIntervalOtherValue = StringUtils.defaultString(otherPolicy.policyMap.get(ruleKey), rule.getDefaultValue());
+                        newPasswordPolicies.put(ruleKey, mergeMin(expirationIntervalLocalValue, expirationIntervalOtherValue));
                         break;
 
                     case MinimumLifetime:
-                        newPasswordPolicies.put(ruleKey, mergeMin(policyMap.get(ruleKey), otherPolicy.policyMap.get(ruleKey)));
+                        final String minimumLifetimeLocalValue = StringUtils.defaultString(policyMap.get(ruleKey), rule.getDefaultValue());
+                        final String minimumLifetimeOtherValue = StringUtils.defaultString(otherPolicy.policyMap.get(ruleKey), rule.getDefaultValue());
+                        newPasswordPolicies.put(ruleKey, mergeMin(minimumLifetimeLocalValue, minimumLifetimeOtherValue));
                         break;
 
                     default:
+                        final String localValueString = StringUtils.defaultString(policyMap.get(ruleKey), rule.getDefaultValue());
+                        final String otherValueString = StringUtils.defaultString(otherPolicy.policyMap.get(ruleKey), rule.getDefaultValue());
+
                         switch (rule.getRuleType()) {
                             case MIN:
-                                newPasswordPolicies.put(ruleKey, mergeMin(policyMap.get(ruleKey), otherPolicy.policyMap.get(ruleKey)));
+                                newPasswordPolicies.put(ruleKey, mergeMin(localValueString, otherValueString));
                                 break;
 
                             case MAX:
-                                newPasswordPolicies.put(ruleKey, mergeMax(policyMap.get(ruleKey), otherPolicy.policyMap.get(ruleKey)));
+                                newPasswordPolicies.put(ruleKey, mergeMax(localValueString, otherValueString));
                                 break;
 
                             case BOOLEAN:
-                                final boolean localValue = StringHelper.convertStrToBoolean(policyMap.get(ruleKey));
-                                final boolean otherValue = StringHelper.convertStrToBoolean(otherPolicy.policyMap.get(ruleKey));
+                                final boolean localValue = StringHelper.convertStrToBoolean(localValueString);
+                                final boolean otherValue = StringHelper.convertStrToBoolean(otherValueString);
 
                                 if (rule.isPositiveBooleanMerge()) {
                                     newPasswordPolicies.put(ruleKey, String.valueOf(localValue || otherValue));
@@ -295,16 +315,16 @@ public class PwmPasswordPolicy implements Profile,Serializable {
             }
         }
 
-        public List<Pattern> getRegExMatch() {
-            return readRegExSetting(PwmPasswordRule.RegExMatch);
+        public List<Pattern> getRegExMatch(MacroMachine macroMachine) {
+            return readRegExSetting(PwmPasswordRule.RegExMatch, macroMachine);
         }
 
-        public List<Pattern> getRegExNoMatch() {
-            return readRegExSetting(PwmPasswordRule.RegExNoMatch);
+        public List<Pattern> getRegExNoMatch(MacroMachine macroMachine) {
+            return readRegExSetting(PwmPasswordRule.RegExNoMatch, macroMachine);
         }
 
         public List<Pattern> getCharGroupValues() {
-            return readRegExSetting(PwmPasswordRule.CharGroupsValues);
+            return readRegExSetting(PwmPasswordRule.CharGroupsValues, null);
         }
 
 
@@ -331,22 +351,34 @@ public class PwmPasswordPolicy implements Profile,Serializable {
             return StringHelper.convertStrToBoolean(value);
         }
 
-        private List<Pattern> readRegExSetting(final PwmPasswordRule rule) {
+        private List<Pattern> readRegExSetting(final PwmPasswordRule rule, 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(value);
+                        final Pattern loopPattern = Pattern.compile(valueToCompile);
                         patterns.add(loopPattern);
                     } catch (PatternSyntaxException e) {
-                        LOGGER.warn("reading password rule value '" + value + "' for rule " + rule.getKey() + " is not a valid regular expression " + e.getMessage());
+                        LOGGER.warn("reading password rule value '" + valueToCompile + "' for rule " + rule.getKey() + " is not a valid regular expression " + e.getMessage());
                     }
                 }
             }

+ 28 - 6
src/main/java/password/pwm/config/profile/PwmPasswordRule.java

@@ -22,17 +22,19 @@
 
 package password.pwm.config.profile;
 
-import com.novell.ldapchai.ChaiPasswordRule;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.Set;
+
+import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Message;
 import password.pwm.util.LocaleHelper;
 import password.pwm.util.logging.PwmLogger;
 
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.MissingResourceException;
-import java.util.Set;
+import com.novell.ldapchai.ChaiPasswordRule;
 
 /**
  * Password rules
@@ -93,6 +95,7 @@ public enum PwmPasswordRule {
     CharGroupsMinMatch          (null                                      ,PwmSetting.PASSWORD_POLICY_CHAR_GROUPS_MIN_MATCH        ,ChaiPasswordRule.RuleType.MIN, "0",false),
     CharGroupsValues            (null                                      ,PwmSetting.PASSWORD_POLICY_CHAR_GROUPS                  ,ChaiPasswordRule.RuleType.OTHER, "",false),
 
+    AllowMacroInRegExSetting    (                                           AppProperty.ALLOW_MACRO_IN_REGEX_SETTING                ,ChaiPasswordRule.RuleType.BOOLEAN, "true", false), 
     ;
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(PwmPasswordRule.class);
@@ -109,6 +112,7 @@ public enum PwmPasswordRule {
 
     private final ChaiPasswordRule chaiPasswordRule;
     private final PwmSetting pwmSetting;
+    private final AppProperty appProperty;
     private final ChaiPasswordRule.RuleType ruleType;
     private final String defaultValue;
     private final boolean positiveBooleanMerge;
@@ -116,19 +120,37 @@ public enum PwmPasswordRule {
     PwmPasswordRule(final ChaiPasswordRule chaiPasswordRule, final PwmSetting pwmSetting, final ChaiPasswordRule.RuleType ruleType, final String defaultValue, final boolean positiveBooleanMerge) {
         this.pwmSetting = pwmSetting;
         this.chaiPasswordRule = chaiPasswordRule;
+        this.appProperty = null;
+        this.ruleType = ruleType;
+        this.defaultValue = defaultValue;
+        this.positiveBooleanMerge = positiveBooleanMerge;
+    }
+
+    PwmPasswordRule(final AppProperty appProperty, final ChaiPasswordRule.RuleType ruleType, final String defaultValue, final boolean positiveBooleanMerge) {
+        this.pwmSetting = null;
+        this.chaiPasswordRule = null;
+        this.appProperty = appProperty;
         this.ruleType = ruleType;
         this.defaultValue = defaultValue;
         this.positiveBooleanMerge = positiveBooleanMerge;
     }
 
     public String getKey() {
-        return null != chaiPasswordRule ? chaiPasswordRule.getKey() : pwmSetting.getKey();
+        if (chaiPasswordRule != null) return chaiPasswordRule.getKey();
+        if (pwmSetting       != null) return pwmSetting.getKey();
+        if (appProperty      != null) return appProperty.getKey();
+
+        return this.name();
     }
 
     public PwmSetting getPwmSetting() {
         return pwmSetting;
     }
 
+    public AppProperty getAppProperty() {
+        return appProperty;
+    }
+
     public ChaiPasswordRule.RuleType getRuleType() {
         return ruleType;
     }

+ 5 - 7
src/main/java/password/pwm/util/PwmPasswordRuleValidator.java

@@ -174,6 +174,9 @@ public class PwmPasswordRuleValidator {
 
         final List<ErrorInformation> errorList = new ArrayList<>();
         final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper();
+        final MacroMachine macroMachine = uiBean == null
+            ? MacroMachine.forNonUserSpecific(pwmApplication, SessionLabel.SYSTEM_LABEL)
+            : MacroMachine.forUser(pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, uiBean.getUserIdentity());
 
         //check against old password
         if (oldPasswordString != null && oldPasswordString.length() > 0 && ruleHelper.readBooleanValue(PwmPasswordRule.DisallowCurrent)) {
@@ -217,11 +220,6 @@ public class PwmPasswordRuleValidator {
 
         // check against disallowed values;
         if (!ruleHelper.getDisallowedValues().isEmpty()) {
-            final MacroMachine macroMachine = uiBean == null
-                    ? MacroMachine.forNonUserSpecific(pwmApplication, SessionLabel.SYSTEM_LABEL)
-                    : MacroMachine.forUser(pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, uiBean.getUserIdentity());
-
-
             final String lcasePwd = passwordString.toLowerCase();
             final Set<String> paramValues = new HashSet<>(ruleHelper.getDisallowedValues());
 
@@ -283,7 +281,7 @@ public class PwmPasswordRuleValidator {
         }
 
         // check regex matches.
-        for (final Pattern pattern : ruleHelper.getRegExMatch()) {
+        for (final Pattern pattern : ruleHelper.getRegExMatch(macroMachine)) {
             if (!pattern.matcher(passwordString).matches()) {
                 errorList.add(new ErrorInformation(PwmError.PASSWORD_INVALID_CHAR));
                 //LOGGER.trace(pwmSession, "password rejected, does not match configured regex pattern: " + pattern.toString());
@@ -295,7 +293,7 @@ public class PwmPasswordRuleValidator {
         }
 
         // check no-regex matches.
-        for (final Pattern pattern : ruleHelper.getRegExNoMatch()) {
+        for (final Pattern pattern : ruleHelper.getRegExNoMatch(macroMachine)) {
             if (pattern.matcher(passwordString).matches()) {
                 errorList.add(new ErrorInformation(PwmError.PASSWORD_INVALID_CHAR));
                 //LOGGER.trace(pwmSession, "password rejected, matches configured no-regex pattern: " + pattern.toString());

+ 1 - 0
src/main/resources/password/pwm/AppProperty.properties

@@ -224,3 +224,4 @@ token.maxUniqueCreateAttempts=100
 urlshortener.url.regex=(https?://([^:@]+(:[^@]+)?@)?([a-zA-Z0-9.]+|d{1,3}.d{1,3}.d{1,3}.d{1,3}|[[0-9a-fA-F:]+])(:d{1,5})?/*[a-zA-Z0-9/\%_.]*?*[a-zA-Z0-9/\%_.=&#]*)
 wordlist.builtin.path=/WEB-INF/wordlist.zip
 ws.restClient.pwRule.haltOnError=true
+password.policy.allowMacroInRegexSetting=true

+ 2 - 2
src/main/resources/password/pwm/i18n/ConfigEditor.properties

@@ -483,8 +483,8 @@ Setting_Description_password.policy.minimumStrength=Password strengths are judge
 Setting_Description_password.policy.minimumUnique=Minimum amount of unique characters allowed.  A value of zero disables this check.
 Setting_Description_password.policy.minimumUpperCase=Minimum amount of uppercase characters required.  A value of zero disables this check.
 Setting_Description_password.policy.queryMatch=Settings here are used to determine if this password policy applies to a given user.  During login, if a previous policy has not yet been assigned to the user, the matches here are considered and if positive, the user will then be assigned this policy.
-Setting_Description_password.policy.regExMatch=A Regular Expression pattern the password must match in order to be allowed.  Multiple patterns can be listed.  A pattern must match the <i>entire</i> password to be applied.  A partial match is ignored.
-Setting_Description_password.policy.regExNoMatch=A Regular Expression pattern the password must <b>not</b> match in order to be allowed.  Multiple patterns can be listed.  A pattern must match the <i>entire</i> password to be applied.  A partial match is ignored.
+Setting_Description_password.policy.regExMatch=A Regular Expression pattern the password must match in order to be allowed.  Multiple patterns can be listed.  A pattern must match the <i>entire</i> password to be applied.  A partial match is ignored.  Macros can be used.
+Setting_Description_password.policy.regExNoMatch=A Regular Expression pattern the password must <b>not</b> match in order to be allowed.  Multiple patterns can be listed.  A pattern must match the <i>entire</i> password to be applied.  A partial match is ignored.  Macros can be used.
 Setting_Description_password.policy.ruleText=When blank, an automatically generated rule list will be displayed to the user. The automated rule list may not be inclusive of all settings in the password policy. Some of the more esoteric or difficult to communicate rules will not appear in the automatically generated list.  This is done in an attempt to not overwhelm the user with having to read and parse the rules before attempting to change the password.  Should the user type a password that conflicts with such a rule - the per-keystroke rule checker will provide direct feedback to the user on how to correct the problem.<br/><br/>If the automatically generated rule list is not desired, it can be overridden by setting a value here.  HTML tags are permitted.
 Setting_Description_password.policy.source=This setting determines where password policy settings should be read from.  If <code>LDAP</code> is selected, an attempt to read the policy out of the ldap directory will be made, and many of the following settings could be ignored.  If <code>Local Config</code> is selected, the policy settings on this page are used, and any policy settings in the LDAP directory are ignored.  If <code>Merge</code> is selected, both policies are read, and where there is any conflict, The application will chose the most restrictive value of the policy.<br/><br/>The capability to read policy from LDAP is only available with some LDAP directory types.  <p>Additionally, the password policy used is only the "local" policy used by this application.  Upon a password set operation, the LDAP directory will typically enforce whatever policies are configured in the directory itself.
 Setting_Description_password.profile.list=List of Password Policy Profiles.  When multiple password policy profiles are configured, all profiles are evaluated in order to check if the setting <code>@PwmSettingReference\:password.policy.queryMatch@</code> matches the user.

+ 77 - 0
src/test/java/password/pwm/config/profile/RuleHelperTest.java

@@ -0,0 +1,77 @@
+package password.pwm.config.profile;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+import static password.pwm.config.profile.PwmPasswordRule.*;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import password.pwm.config.profile.PwmPasswordPolicy.RuleHelper;
+import password.pwm.util.macro.MacroMachine;
+
+public class RuleHelperTest {
+    private static final String[][] MACRO_MAP = new String[][] {
+        { "@User:ID@",        "fflintstone"         },
+        { "@User:Email@",     "fred@flintstones.tv" },
+        { "@LDAP:givenName@", "Fred"                },
+        { "@LDAP:sn@",        "Flintstone"          }
+    };
+
+    private MacroMachine macroMachine = mock(MacroMachine.class);
+    private RuleHelper ruleHelper = mock(RuleHelper.class);
+
+    @Before
+    public void setUp() throws Exception {
+        // Mock out things that don't need to be real
+        when(macroMachine.expandMacros(anyString())).thenAnswer(replaceAllMacrosInMap(MACRO_MAP));
+        when(ruleHelper.readBooleanValue(PwmPasswordRule.AllowMacroInRegExSetting)).thenReturn(Boolean.TRUE);
+        when(ruleHelper.readRegExSetting(any(PwmPasswordRule.class), any(MacroMachine.class), anyString())).thenCallRealMethod();
+    }
+
+    @Test
+    public void testReadRegExSetting_noRegex() throws Exception {
+        final String input = "@User:ID@, First Name: @LDAP:givenName@, Last Name: @LDAP:sn@, Email: @User:Email@";
+
+        final List<Pattern> patterns = ruleHelper.readRegExSetting(RegExMatch, macroMachine, input);
+
+        assertThat(patterns.size()).isEqualTo(1);
+        assertThat(patterns.get(0).pattern()).isEqualTo("fflintstone, First Name: Fred, Last Name: Flintstone, Email: fred@flintstones.tv");
+    }
+
+    @Test
+    public void testReadRegExSetting() throws Exception {
+        final String input = "^@User:ID@[0-9]+$;;;^password$";
+
+        final List<Pattern> patterns = ruleHelper.readRegExSetting(RegExMatch, macroMachine, input);
+
+        assertThat(patterns.size()).isEqualTo(2);
+        assertThat(patterns.get(0).pattern()).isEqualTo("^fflintstone[0-9]+$");
+        assertThat(patterns.get(1).pattern()).isEqualTo("^password$");
+    }
+
+    private Answer<String> replaceAllMacrosInMap(final String[][] macroMap) {
+        return new Answer<String>() {
+            @Override
+            public String answer(InvocationOnMock invocation) throws Throwable {
+                final String[] macroNames = new String[macroMap.length];
+                final String[] macroValues = new String[macroMap.length];
+
+                for (int i=0; i<macroMap.length; i++) {
+                    macroNames[i] = macroMap[i][0];
+                    macroValues[i] = macroMap[i][1];
+                }
+
+                final String stringWithMacros = invocation.getArgumentAt(0, String.class);
+                return StringUtils.replaceEach(stringWithMacros, macroNames, macroValues);
+            }
+        };
+    }
+}