Problem/Motivation

The current strategy is tightly coupled into Form API validation callback. Client applications, using REST or JSONAPI, should be able to apply the validation logic inside HOOK_user_presave().

Expected Results:

When a client application creates a user that does not meet the password policy constraints, a descriptive exception is thrown, and processed by the client.

Actual Result:

When a client application creates a user that does not meet the password policy constraints, the user is saved.


In Progress Resolution

  public function presaveUserValidate(User $entity) {

    $request = \Drupal\Component\Serialization\Json::decode(\Drupal::request()->getContent());
    $pass = $request['data']['attributes']['pass'];

    //@todo get role
    $roles = $entity->getRoles();
    if (empty($roles)) {
      $roles = ['authenticated' => 'authenticated'];
    }

    // Run validation.
    $applicable_policies = [];
    $ids = [];
    foreach ($roles as $role_key => $role_enabled) {
      if ($role_enabled) {
        $role_map = ['roles.' . $role_enabled => $role_enabled];
        $role_policies = \Drupal::entityTypeManager()->getStorage('password_policy')->loadByProperties($role_map);
        /** @var \Drupal\password_policy\Entity\PasswordPolicy $policy */
        foreach ($role_policies as $policy) {
          if (!in_array($policy->id(), $ids)) {
            $applicable_policies[] = $policy;
            $ids[] = $policy->id();
          }
        }
      }
    }

    // Run validation.
    $failed = FALSE;
    $force_failure = FALSE;

    // Process user context
    // TODO - Turn this into configuration.
    $user_context_fields = ['mail', 'name', 'uid'];
    $user_context_values = [];
    foreach ($user_context_fields as $user_context_field) {
      if($entity->hasField($user_context_field)) {
        $user_context_values[$user_context_field] = $entity->get($user_context_field)->value;
      }
    }

    /** @var \Drupal\password_policy\Entity\PasswordPolicy $policy */
    foreach ($applicable_policies as $policy_id => $policy) {
      $policy_constraints = $policy->getConstraints();

      foreach ($policy_constraints as $constraint_id => $constraint) {

        $plugin_inst = \Drupal::service('plugin.manager.password_policy.password_constraint');
        $plugin_object = $plugin_inst->createInstance($constraint['id'], $constraint);

        // Execute validation.
        $validation = $plugin_object->validate($pass, $user_context_values);

        if ($validation->isValid() && !$force_failure) {
          $status = t('Pass');
        }
        else {
          $message = $validation->getErrorMessage();
          $failed = TRUE;
        }

      }
    }

    if($failed) {
      throw new AccessDeniedHttpException('There is an issue with the password');
    }
    return;
  }
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

connorhoehn created an issue. See original summary.

kurtfoster’s picture

I hoping to get time to look at this soon as well. I think the validation should actually be removed from the form validate function so that it is callable and usable by other code using this module. The validate function can then be used in the form validation and in an entity presave.

kurtfoster’s picture

Here's my first roll of a patch for this issue, it needs work. It's against 8.x-3.0-alpha4+11-dev

kurtfoster’s picture

There was a small error in that patch, updated.

malks’s picture

Hey @kurtfoster, I've rerolled this patch with the check on the existing user removed. The logic being that for a non form update of a user, e.g. via an API the user updating will never be the same as the user being updated so the validation will never run. Let me know what you think.

malks’s picture

Added a small fix to the patch where the wrong value was being passed to the validators.

yovince’s picture

1. removed PasswordPolicyConstraintValidator.php line86-101
not sure why do we need $user_context_values.

    // Process user context
    // TODO - Turn this into configuration.
    $user_context_fields = ['mail', 'name', 'uid'];
    $user_context_values = [];
    foreach ($user_context_fields as $user_context_field) {
      $user_context_values[$user_context_field] = $entity->get($user_context_field)->getValue();

      if ($user_context_field == 'uid') {
        $user_context_values[$user_context_field] = \Drupal::routeMatch()->getRawParameter('user');
      }
      // Check default value.

      if (empty($user_context_values[$user_context_field]) and !empty($form['account'][$user_context_field]['#default_value'])) {
        $user_context_values[$user_context_field] = $form['account'][$user_context_field]['#default_value'];
      }
    }

2. line 131

$validation = $plugin_object->validate($password, $user_context_values);

the validate() accepts a string and an `UserInterface` entity. so passing an array into the function which seems not right.

yovince’s picture

Nadim Hossain’s picture

Rerolled the patch to be compatible with php 8.1 upgrade.
just one minor change. From if ($role_enabled['target_id']) to if (isset($role_enabled['target_id']))

Thanks Anas for pointing out the similar patch and looks like the other patch has more possibility to get merged. I will apply the other patch you mentioned in our project and do some testing, after that will be closing this issue.

Kristen Pol’s picture

Status: Active » Postponed (maintainer needs more info)

Thanks to everyone for the work on this issue.

I'm going through all the 8.x issues.

As the 8.x is no longer supported, I'm postponing this issue for now and need feedback as to whether or not this issue is relevant to 4.0.x.

If it is, please reopen and change the version, make sure the issue summary is clear and complete, including steps to reproduce, and reroll the patch. If it's not, please close.

If there is no response to this in a month addressing the above, it can be closed.