nbonniot nbonniot - 5 months ago 25
PHP Question

Symfony2 FOS User password check callback constraint

I'm using FOS User Bundle on a multisite platform I created.
I need to setup custom constraints on password check depending on user role (stronger regex check on admin users).

How and where can I hook to safely do this? Assert/Constraint seems not usable on User entity, such as this answer which not allow different check regarding roles.

Thanks for your answers,

Nicolas

Answer

You could create a custom validation constraint and attach it to the class rather than the property.

Acme\UserBundle\Validator\Constraints\PasswordForRoleRegex

namespace Acme\UserBundle\Validator\Constraints;

/**
 * @Annotation
 * @Target({"CLASS", "ANNOTATION"})
 */
class PasswordForRoleRegex extends Constraint
{
    /**
     * {@inheritdoc}
     */
    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }
}

Acme\UserBundle\Validator\Constraints\PasswordForRoleRegexValidator

namespace Acme\UserBundle\Validator\Constraints;

class PasswordForRoleRegexValidator extends ConstraintValidator
{
    const REGEX_SUPER_ADMIN = '/..../';
    // or an actual message if you don't use translations
    const MESSAGE_SUPER_ADMIN = 'acme.user.password.regex.super_admin';

    const REGEX_ADMIN = '/..../';
    const MESSAGE_ADMIN = 'acme.user.password.regex.admin';

    /// and so on

    const REGEX_NORMAL_USER = '/..../';
    const MESSAGE_NORMAL_USER = 'acme.user.password.regex.normal_user';


    public function validate($user, Constraint $constraint)
    {
        if (!$constraint instanceof PasswordForRoleRegex) {
            throw new UnexpectedTypeException($constraint, PasswordForRoleRegex::class);
        }

        if (!$user instanceof UserInterface) {
            throw new UnexpectedTypeException($user, UserInterface::class);
        }

        if (null === $password = $user->getPlainPassword()) {
            return;
        }

        if (preg_match($this->getPasswordRegexForUserRole($user), $password) {
            return;
        }

        $this->context->buildViolation($this->getErrorMessageForUserRole($user))
            ->atPath('plainPassword')
            ->addViolation();
    }

    /**
     * @param UserInterface $user
     * @return string
     */
    private function getPasswordRegexForUserRole(UserInterface $user)
    {
        if ($user->hasRole('ROLE_SUPER_ADMIN')) {
            return self::REGEX_SUPER_ADMIN;
        }

        if ($user->hasRole('ROLE_ADMIN')) {
            return self::REGEX_ADMIN;
        }

        // and so on

        return self::REGEX_NORMAL_USER;
    }

    /**
     * @param UserInterface $user
     * @return string
     */
    private function getErrorMessageForUserRole(UserInterface $user)
    {
        if ($user->hasRole('ROLE_SUPER_ADMIN')) {
            return self::MESSAGE_SUPER_ADMIN;
        }

        if ($user->hasRole('ROLE_ADMIN')) {
            return self::MESSAGE_ADMIN;
        }

        // and so on

        return self::MESSAGE_NORMAL_USER;
    }
}

Which you could then use in your validation like...

Acme\UserBundle\Model\User

namespace Acme\UserBundle\Model;

use Acme\UserBundle\Validator\Constraints\PasswordForRoleRegex;
use FOS\UserBundle\Model\User as BaseUser;

/**
 * @PasswordForRoleRegex(groups={"Registration", "ChangePassword", ....})
 */
class User extends BaseUser
{
    //...
}

or..

@AcmeUserBundle/Resources/config/validation.yml

Acme\UserBundle\Model\User:
    properties:
        Acme\UserBundle\Validator\Constraints\PasswordForRoleRegex:
            groups: ["Registration", "ChangePassword", ....]

or XML that i can't be bothered to do.

I'm pretty sure this would work but it's not tested so it may not be 100%.