From a00e82adec8a13d514f9aa29f56762591426bd5e Mon Sep 17 00:00:00 2001
From: Ludovic Poitou <ludovic.poitou@forgerock.com>
Date: Thu, 30 Jan 2014 13:50:01 +0000
Subject: [PATCH] Fix for OPENDJ-1295, allow password policy subentries to refer to configured password validators. To do so, one will need to add an auxiliary objectclass in the password policy subentry and the mandatory ds-cfg-password-validator attribute (value is the DN of the validator in cn=config DIT).

---
 opends/src/messages/messages/core.properties                                                         |    9 +
 opends/resource/schema/03-pwpolicyextension.ldif                                                     |   33 +++++
 opends/src/server/org/opends/server/core/SubentryPasswordPolicy.java                                 |  165 ++++++++++++++++++++++----
 opends/tests/unit-tests-testng/src/server/org/opends/server/core/SubentryPasswordPolicyTestCase.java |  142 +++++++++++++++++++++++
 4 files changed, 317 insertions(+), 32 deletions(-)

diff --git a/opends/resource/schema/03-pwpolicyextension.ldif b/opends/resource/schema/03-pwpolicyextension.ldif
new file mode 100644
index 0000000..35250d4
--- /dev/null
+++ b/opends/resource/schema/03-pwpolicyextension.ldif
@@ -0,0 +1,33 @@
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License, Version 1.0 only
+# (the "License").  You may not use this file except in compliance
+# with the License.
+#
+# You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+# or http://forgerock.org/license/CDDLv1.0.html.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at legal-notices/CDDLv1_0.txt.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information:
+#      Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+#
+#      Copyright 2014 ForgeRock AS.
+#
+#
+# This file contains the attribute type and objectclass definitions for use
+# with the Directory Server configuration.
+dn: cn=schema
+objectClass: top
+objectClass: ldapSubentry
+objectClass: subschema
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.18 NAME 'pwdValidatorPolicy' SUP top AUXILIARY
+  MUST (ds-cfg-password-validator) X-ORIGIN 'OpenDJ Directory Server' )
diff --git a/opends/src/messages/messages/core.properties b/opends/src/messages/messages/core.properties
index b31222f..a03040b 100644
--- a/opends/src/messages/messages/core.properties
+++ b/opends/src/messages/messages/core.properties
@@ -1421,3 +1421,12 @@
 MILD_ERR_MODIFY_REPLACE_INVALID_SYNTAX_NO_VALUE_744=When attempting to modify \
  entry %s to replace the set of values for attribute %s, one value was found to \
  be invalid according to the associated syntax:  %s
+SEVERE_ERR_PWPOLICY_UNKNOWN_VALIDATOR_745=The password policy \
+definition contained in configuration entry "%s" is invalid because the \
+password validator "%s" specified in attribute "%s" cannot be found
+SEVERE_ERR_PWPOLICY_REJECT_DUE_TO_UNKNOWN_VALIDATOR_REASON_746=The password could not \
+ be validated because of misconfiguration. Please contact the administrator
+SEVERE_ERR_PWPOLICY_REJECT_DUE_TO_UNKNOWN_VALIDATOR_LOG_747=The password for \
+ user %s could not be validated because the password policy subentry %s is \
+ referring to an unknown password validator (%s). Please make sure the password \
+ policy subentry only refers to validators that exist on all replicas
\ No newline at end of file
diff --git a/opends/src/server/org/opends/server/core/SubentryPasswordPolicy.java b/opends/src/server/org/opends/server/core/SubentryPasswordPolicy.java
index 4d3973c..2fa308d 100644
--- a/opends/src/server/org/opends/server/core/SubentryPasswordPolicy.java
+++ b/opends/src/server/org/opends/server/core/SubentryPasswordPolicy.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2013 ForgeRock AS.
+ *      Portions copyright 2011-2014 ForgeRock AS.
  */
 
 package org.opends.server.core;
@@ -31,17 +31,22 @@
 
 import static org.opends.messages.ConfigMessages.*;
 import static org.opends.messages.CoreMessages.*;
+import static org.opends.server.loggers.ErrorLogger.logError;
 import static org.opends.server.loggers.debug.DebugLogger.*;
 import static org.opends.server.schema.SchemaConstants.*;
 
 import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.opends.messages.Message;
+import org.opends.messages.MessageBuilder;
 import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn.*;
+import org.opends.server.admin.std.server.PasswordValidatorCfg;
 import org.opends.server.api.AccountStatusNotificationHandler;
 import org.opends.server.api.PasswordGenerator;
 import org.opends.server.api.PasswordStorageScheme;
 import org.opends.server.api.PasswordValidator;
+import org.opends.server.config.ConfigException;
 import org.opends.server.loggers.debug.DebugTracer;
 import org.opends.server.types.*;
 
@@ -82,35 +87,41 @@
   private static final String PWD_ATTR_FAILURECOUNTINTERVAL =
       "pwdfailurecountinterval";
 
-  // Password Policy Subentry DN.
-  private final DN passwordPolicySubentryDN;
-  // The value of the "allow-user-password-changes" property.
-  private final Boolean pAllowUserPasswordChanges;
-  // The value of the "force-change-on-reset" property.
-  private final Boolean pForceChangeOnReset;
-  // The value of the "grace-login-count" property.
-  private final Integer pGraceLoginCount;
-  // The value of the "lockout-duration" property.
-  private final Long pLockoutDuration;
-  // The value of the "lockout-failure-count" property.
-  private final Integer pLockoutFailureCount;
-  // The value of the "lockout-failure-expiration-interval" property.
-  private final Long pLockoutFailureExpirationInterval;
-  // The value of the "max-password-age" property.
-  private final Long pMaxPasswordAge;
-  // The value of the "min-password-age" property.
-  private final Long pMinPasswordAge;
-  // The value of the "password-attribute" property.
-  private final AttributeType pPasswordAttribute;
-  // The value of the "password-change-requires-current-password" property.
-  private final Boolean pPasswordChangeRequiresCurrentPassword;
-  // The value of the "password-expiration-warning-interval" property.
-  private final Long pPasswordExpirationWarningInterval;
-  // The value of the "password-history-count" property.
-  private final Integer pPasswordHistoryCount;
-  // Indicates if the password attribute uses auth password syntax.
-  private final Boolean pAuthPasswordSyntax;
+  private static final String PWD_ATTR_VALIDATOR = "ds-cfg-password-validator";
+  private static final String PWD_OC_VALIDATORPOLICY = "pwdvalidatorpolicy";
 
+  /** Password Policy Subentry DN. */
+  private final DN passwordPolicySubentryDN;
+  /** The value of the "allow-user-password-changes" property. */
+  private final Boolean pAllowUserPasswordChanges;
+  /** The value of the "force-change-on-reset" property. */
+  private final Boolean pForceChangeOnReset;
+  /** The value of the "grace-login-count" property. */
+  private final Integer pGraceLoginCount;
+  /** The value of the "lockout-duration" property. */
+  private final Long pLockoutDuration;
+  /** The value of the "lockout-failure-count" property. */
+  private final Integer pLockoutFailureCount;
+  /** The value of the "lockout-failure-expiration-interval" property. */
+  private final Long pLockoutFailureExpirationInterval;
+  /** The value of the "max-password-age" property. */
+  private final Long pMaxPasswordAge;
+  /** The value of the "min-password-age" property. */
+  private final Long pMinPasswordAge;
+  /** The value of the "password-attribute" property. */
+  private final AttributeType pPasswordAttribute;
+  /** The value of the "password-change-requires-current-password" property. */
+  private final Boolean pPasswordChangeRequiresCurrentPassword;
+  /** The value of the "password-expiration-warning-interval" property. */
+  private final Long pPasswordExpirationWarningInterval;
+  /** The value of the "password-history-count" property. */
+  private final Integer pPasswordHistoryCount;
+  /** Indicates if the password attribute uses auth password syntax. */
+  private final Boolean pAuthPasswordSyntax;
+  /** The set of password validators if any. */
+  private final Set<DN> pValidatorNames = new HashSet<DN>();
+  /** Used when logging errors due to invalid validator reference. */
+  private final AtomicBoolean isAlreadyLogged = new AtomicBoolean();
 
 
   // Returns the global default password policy which will be used for deriving
@@ -500,6 +511,36 @@
     {
       this.pLockoutFailureExpirationInterval = null;
     }
+
+    // Now check for the pwdValidatorPolicy OC and its attribute.
+    // Determine if this is a password validator policy object class.
+    ObjectClass pwdValidatorPolicyOC =
+        DirectoryServer.getObjectClass(PWD_OC_VALIDATORPOLICY);
+    if (pwdValidatorPolicyOC != null &&
+        objectClasses.containsKey(pwdValidatorPolicyOC))
+    {
+      AttributeType pwdAttrType =
+          DirectoryServer.getAttributeType(PWD_ATTR_VALIDATOR, true);
+      List<Attribute> pwdAttrList = entry.getAttribute(pwdAttrType);
+      if ((pwdAttrList != null) && (!pwdAttrList.isEmpty()))
+      {
+        for (Attribute attr : pwdAttrList)
+        {
+          for (AttributeValue val : attr)
+          {
+            DN validatorDN = DN.decode(val.getValue());
+            if (DirectoryServer.getPasswordValidator(validatorDN) == null)
+            {
+              throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                  ERR_PWPOLICY_UNKNOWN_VALIDATOR.get(
+                      this.passwordPolicySubentryDN.toNormalizedString(),
+                      validatorDN.toString(), PWD_ATTR_VALIDATOR));
+            }
+            pValidatorNames.add(validatorDN);
+          }
+        }
+      }
+    }
   }
 
 
@@ -964,10 +1005,76 @@
   @Override
   public Collection<PasswordValidator<?>> getPasswordValidators()
   {
+    if (!pValidatorNames.isEmpty())
+    {
+      Collection<PasswordValidator<?>> values =
+          new HashSet<PasswordValidator<?>>();
+      for (DN validatorDN : pValidatorNames){
+        PasswordValidator<?> validator = DirectoryServer
+            .getPasswordValidator(validatorDN);
+        if (validator == null) {
+          PasswordValidator<?> errorValidator = new RejectPasswordValidator(
+              validatorDN.toString(), passwordPolicySubentryDN.toString());
+          values.clear();
+          values.add(errorValidator);
+          return values;
+        }
+         values.add(validator);
+      }
+      isAlreadyLogged.set(false);
+      return values;
+    }
     return getDefaultPasswordPolicy().getPasswordValidators();
   }
 
+  /**
+   * Implementation of a specific Password Validator that reject all
+   * password due to mis-configured password policy subentry.
+   * This is only used when a subentry is referencing a password
+   * validator that is no longer configured.
+   */
+  private final class RejectPasswordValidator extends
+      PasswordValidator<PasswordValidatorCfg>
+  {
+    private final String validatorName;
+    private final String pwPolicyName;
+    public RejectPasswordValidator(String name, String policyName)
+    {
+      super();
+      validatorName = name;
+      pwPolicyName = policyName;
+    }
+    /**
+     * {@inheritDoc}
+     */
+    @Override()
+    public void initializePasswordValidator(PasswordValidatorCfg configuration)
+        throws ConfigException
+    {
+       // do nothing
+    }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override()
+    public boolean passwordIsAcceptable(ByteString newPassword,
+                                        Set<ByteString> currentPasswords,
+                                        Operation operation, Entry userEntry,
+                                        MessageBuilder invalidReason)
+    {
+      invalidReason.append(ERR_PWPOLICY_REJECT_DUE_TO_UNKNOWN_VALIDATOR_REASON
+          .get());
+
+      // Only log an error once, on first error
+      if (isAlreadyLogged.compareAndSet(false, true)) {
+        logError(ERR_PWPOLICY_REJECT_DUE_TO_UNKNOWN_VALIDATOR_LOG.get(
+          userEntry.getDN().toNormalizedString(), pwPolicyName,
+          validatorName));
+      }
+      return false;
+    }
+  }
 
   /**
    * {@inheritDoc}
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/core/SubentryPasswordPolicyTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/core/SubentryPasswordPolicyTestCase.java
index a5c9cbb..0ea24f7 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/core/SubentryPasswordPolicyTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/core/SubentryPasswordPolicyTestCase.java
@@ -22,14 +22,16 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011 ForgeRock AS.
+ *      Portions copyright 2011-2014 ForgeRock AS.
  */
 package org.opends.server.core;
 
 
 
+import java.util.Collection;
 import java.util.List;
 
+import org.opends.server.api.PasswordValidator;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
@@ -191,8 +193,47 @@
          "pwdLockoutDuration: 2147483648",
          "pwdMaxFailure: 3",
          "pwdMustChange: TRUE",
-         "pwdAttribute: userPassword"
-         );
+         "pwdAttribute: userPassword",
+         "",
+         "dn: cn=Temp Policy 6," + SUFFIX,
+         "objectClass: top",
+         "objectClass: pwdPolicy",
+         "objectClass: pwdValidatorPolicy",
+         "objectClass: subentry",
+         "cn: Temp Policy 6",
+         "subtreeSpecification: { base \"ou=people\" }",
+         "pwdLockoutDuration: 300",
+         "pwdMaxFailure: 3",
+         "pwdMustChange: TRUE",
+         "pwdAttribute: userPassword",
+         "",
+         "dn: cn=Temp Policy 7," + SUFFIX,
+         "objectClass: top",
+         "objectClass: pwdPolicy",
+         "objectClass: pwdValidatorPolicy",
+         "objectClass: subentry",
+         "cn: Temp Policy 7",
+         "subtreeSpecification: { base \"ou=people\" }",
+         "pwdLockoutDuration: 300",
+         "pwdMaxFailure: 3",
+         "pwdMustChange: TRUE",
+         "pwdAttribute: userPassword",
+         "ds-cfg-password-validator: Not_A_DN",
+         "",
+         "dn: cn=Temp Policy 8," + SUFFIX,
+         "objectClass: top",
+         "objectClass: pwdPolicy",
+         "objectClass: pwdValidatorPolicy",
+         "objectClass: subentry",
+         "cn: Temp Policy 8",
+         "subtreeSpecification: { base \"ou=people\" }",
+         "pwdLockoutDuration: 300",
+         "pwdMaxFailure: 3",
+         "pwdMustChange: TRUE",
+         "pwdAttribute: userPassword",
+         "ds-cfg-password-validator: cn=Unique Characters Inexistant,cn=Password Validators,cn=config"
+
+    );
 
     Object[][] configEntries = new Object[entries.size()][1];
     for (int i=0; i < configEntries.length; i++)
@@ -297,6 +338,17 @@
     assertEquals(policy.isAllowUserPasswordChanges(), false);
     assertEquals(policy.isPasswordChangeRequiresCurrentPassword(), true);
 
+    /* Check the password validator attributes for correct values.
+     * The default unit-test config has a single Password validator which is
+     * enabled for the default password policy.
+     */
+    Collection<PasswordValidator<?>> validators = policy.getPasswordValidators();
+    assertEquals(validators.size(), 1);
+    for (PasswordValidator<?> validator : validators)
+    {
+      assertTrue(validator.toString().startsWith("org.opends.server.extensions.TestPasswordValidator"));
+    }
+
     // Make sure this policy applies to the test entry
     // its supposed to target and that its the same
     // policy object as previously tested.
@@ -318,6 +370,88 @@
   }
 
   /**
+   * Ensures that password policy constructed from subentry,
+   * containing a password validator reference,
+   * is active and has a valid configuration.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testValidConfigurationWithValidator()
+      throws Exception
+  {
+    PasswordPolicy defaultPolicy =
+        DirectoryServer.getDefaultPasswordPolicy();
+    assertNotNull(defaultPolicy);
+
+    // The values are selected on a basis that they
+    // should differ from default password policy.
+    Entry policyEntry = TestCaseUtils.makeEntry(
+        "dn: cn=Temp Validator Policy," + SUFFIX,
+        "objectClass: top",
+        "objectClass: pwdPolicy",
+        "objectClass: pwdValidatorPolicy",
+        "objectClass: subentry",
+        "cn: Temp Policy",
+        "subtreeSpecification: { base \"ou=people\" }",
+        "pwdLockoutDuration: 300",
+        "pwdMaxFailure: 3",
+        "pwdMustChange: TRUE",
+        "pwdAttribute: authPassword",
+        "pwdMinAge: 600",
+        "pwdMaxAge: 2147483647",
+        "pwdInHistory: 5",
+        "pwdExpireWarning: 864000",
+        "pwdGraceAuthNLimit: 3",
+        "pwdFailureCountInterval: 3600",
+        "pwdAllowUserChange: FALSE",
+        "pwdSafeModify: TRUE",
+        "ds-cfg-password-validator: cn=Unique Characters,cn=Password Validators,cn=config",
+        "ds-cfg-password-validator: cn=Length-Based Password Validator,cn=Password Validators,cn=config"
+    );
+
+    InternalClientConnection connection =
+        InternalClientConnection.getRootConnection();
+
+    AddOperation addOperation =
+        connection.processAdd(policyEntry.getDN(),
+            policyEntry.getObjectClasses(),
+            policyEntry.getUserAttributes(),
+            policyEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+    assertNotNull(DirectoryServer.getEntry(policyEntry.getDN()));
+
+    PasswordPolicy policy = (PasswordPolicy) DirectoryServer.getAuthenticationPolicy(
+        DN.decode("cn=Temp Validator Policy," + SUFFIX));
+    assertNotNull(policy);
+
+    // Check the password validator attributes for correct values.
+    Collection<PasswordValidator<?>> validators = policy.getPasswordValidators();
+    assertFalse(validators.isEmpty());
+    assertEquals(validators.size(), 2);
+
+    // Make sure this policy applies to the test entry
+    // its supposed to target and that its the same
+    // policy object as previously tested.
+    Entry testEntry = DirectoryServer.getEntry(DN.decode(
+        "uid=rogasawara," + BASE));
+    assertNotNull(testEntry);
+
+    AuthenticationPolicy statePolicy = AuthenticationPolicy.forUser(testEntry,
+        false);
+    assertNotNull(statePolicy);
+    assertEquals(policy, statePolicy);
+
+    // Make sure this policy is gone and default
+    // policy is in effect instead.
+    TestCaseUtils.deleteEntry(policyEntry.getDN());
+    statePolicy = AuthenticationPolicy.forUser(testEntry, false);
+    assertNotNull(statePolicy);
+    assertEquals(defaultPolicy, statePolicy);
+  }
+
+
+  /**
    * Ensures that password policy pwdPolicySubentry
    * operational attribute reflects active password
    * policy for a given user entry.
@@ -397,4 +531,6 @@
             defaultPolicy.getDN(
             ).toString())));
   }
+
+
 }

--
Gitblit v1.10.0