Reputation: 49
I have 2 questions/concerns
when I edit a user I don't always want to have to change the user's password, how can I change that?
how can I save the password encrypted in the database, so far I only succeed in plain text and none of the instructions I have found is up to date and / or has helped me.
All other files are created via the command and so far unchanged.
I use Symfony 5.2.7 and php 8.0.6
src/Controller/Admin/AdminCrudController.php
<?php
namespace App\Controller\Admin;
use App\Entity\Admin;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ArrayField;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class AdminCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Admin::class;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setEntityPermission('ROLE_ADMIN')
;
}
public function configureFields(string $pageName): iterable
{
yield TextField::new('username');
yield TextField::new('password')
->hideOnIndex()
->setFormType(PasswordType::class)
;
yield ArrayField::new('roles');
}
}
src/Entity/Admin.php
<?php
namespace App\Entity;
use App\Repository\AdminRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=AdminRepository::class)
*/
class Admin implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
public function __toString(): string
{
return $this->username;
}
public function getId(): ?int
{
return $this->id;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
*
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
Upvotes: 3
Views: 5128
Reputation: 11185
With the excellent answer from @luismi and the comment from @mysiar, I now use this
.. on the User class, I added a stub property that is not persisted in doctrine
class User {
...
/**
* @var string The new plain text password
*/
private $newPassword;
...
public function getNewPassword(): string
{
return (string) $this->newPassword;
}
public function setNewPassword($newPassword): self
{
$this->newPassword = $newPassword;
return $this;
}
On the UserCrudController, I inject the password hasher
class UserCrudController extends AbstractCrudController
{
private UserPasswordHasherInterface $hasher;
public function __construct(
UserPasswordHasherInterface $hasher,
...
) {
...
$this->hasher = $hasher;
}
On the same controller, I add a field for this new password
public function configureFields(string $pageName): iterable
{
$newPassword = Field::new( 'newPassword', 'Password' )
->setFormType( RepeatedType::class )
->setFormTypeOptions( [
'type' => PasswordType::class,
'first_options' => [ 'label' => 'Password' ],
'second_options' => [ 'label' => 'Repeat password' ],
'error_bubbling' => true,
'invalid_message' => 'The password fields do not match.',
] );
....
if (Crud::PAGE_INDEX === $pageName) {
return [$id, $username, $roles];
} elseif (Crud::PAGE_DETAIL === $pageName) {
return [$id, $username, $roles];
} elseif (Crud::PAGE_NEW === $pageName) {
return [$username, $newPassword->setRequired( true ), $roles];
} elseif (Crud::PAGE_EDIT === $pageName) {
return [$username, $newPassword->setRequired( false ), $roles];
}
in the same controller, I add an event listener to the create and edit forms. if the user filled out the newPassword fields, it uses that to set the password field.
public function createNewFormBuilder(
EntityDto $entityDto,
KeyValueStore $formOptions,
AdminContext $context
): FormBuilderInterface {
$formBuilder = parent::createNewFormBuilder( $entityDto, $formOptions, $context );
$this->addEncodePasswordEventListener( $formBuilder );
return $formBuilder;
}
public function createEditFormBuilder(
EntityDto $entityDto,
KeyValueStore $formOptions,
AdminContext $context
): FormBuilderInterface {
$formBuilder = parent::createEditFormBuilder( $entityDto, $formOptions, $context );
$this->addEncodePasswordEventListener( $formBuilder);
return $formBuilder;
}
protected function addEncodePasswordEventListener(
FormBuilderInterface $formBuilder
): void {
$formBuilder->addEventListener(
FormEvents::SUBMIT,
function ( FormEvent $event ) {
$user = $event->getData();
$plainPassword = $user->getNewPassword();
if ($plainPassword != null) {
$user->setPassword( $this->hasher->hashPassword( $user, $plainPassword ) );
$this->get('session')->getFlashBag()->add('success', 'Updated password');
}
}
);
}
Upvotes: 3
Reputation: 365
This is what I'm actually using on Symfony 5.4 + EasyAdmin 4 + php 8.1.1. Works for me on edit user and new user actions. In the New User action password field is required. In the Edit User action it is not. You can pass a blank password and the current one won't be changed.
<?php
#Controller/Admin/UserCrudController.php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\Field;
use EasyCorp\Bundle\EasyAdminBundle\Field\FormField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class UserCrudController extends AbstractCrudController {
private UserPasswordHasherInterface $passwordEncoder;
public function __construct( UserPasswordHasherInterface $passwordEncoder ) {
$this->passwordEncoder = $passwordEncoder;
}
public static function getEntityFqcn(): string {
return User::class;
}
public function configureFields( string $pageName ): iterable {
yield FormField::addPanel( 'User data' )->setIcon( 'fa fa-user' );
yield EmailField::new( 'email' )->onlyWhenUpdating()->setDisabled();
yield EmailField::new( 'email' )->onlyWhenCreating();
yield TextField::new( 'email' )->onlyOnIndex();
$roles = [ 'ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_USER' ];
yield ChoiceField::new( 'roles' )
->setChoices( array_combine( $roles, $roles ) )
->allowMultipleChoices()
->renderAsBadges();
yield FormField::addPanel( 'Change password' )->setIcon( 'fa fa-key' );
yield Field::new( 'password', 'New password' )->onlyWhenCreating()->setRequired( true )
->setFormType( RepeatedType::class )
->setFormTypeOptions( [
'type' => PasswordType::class,
'first_options' => [ 'label' => 'New password' ],
'second_options' => [ 'label' => 'Repeat password' ],
'error_bubbling' => true,
'invalid_message' => 'The password fields do not match.',
] );
yield Field::new( 'password', 'New password' )->onlyWhenUpdating()->setRequired( false )
->setFormType( RepeatedType::class )
->setFormTypeOptions( [
'type' => PasswordType::class,
'first_options' => [ 'label' => 'New password' ],
'second_options' => [ 'label' => 'Repeat password' ],
'error_bubbling' => true,
'invalid_message' => 'The password fields do not match.',
] );
}
public function createEditFormBuilder( EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context ): FormBuilderInterface {
$plainPassword = $entityDto->getInstance()?->getPassword();
$formBuilder = parent::createEditFormBuilder( $entityDto, $formOptions, $context );
$this->addEncodePasswordEventListener( $formBuilder, $plainPassword );
return $formBuilder;
}
public function createNewFormBuilder( EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context ): FormBuilderInterface {
$formBuilder = parent::createNewFormBuilder( $entityDto, $formOptions, $context );
$this->addEncodePasswordEventListener( $formBuilder );
return $formBuilder;
}
protected function addEncodePasswordEventListener( FormBuilderInterface $formBuilder, $plainPassword = null ): void {
$formBuilder->addEventListener( FormEvents::SUBMIT, function ( FormEvent $event ) use ( $plainPassword ) {
/** @var User $user */
$user = $event->getData();
if ( $user->getPassword() !== $plainPassword ) {
$user->setPassword( $this->passwordEncoder->hashPassword( $user, $user->getPassword() ) );
}
} );
}
}
In the User class I've set password as nullable:
<?php
#Entity/User.php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity( repositoryClass: UserRepository::class )]
class User implements UserInterface, PasswordAuthenticatedUserInterface {
#[ORM\Column( type: 'string', nullable: true )]
private ?string $password = null;
public function getPassword(): ?string {
return $this->password;
}
public function setPassword( ?string $password ): self {
if (!is_null($password)) {
$this->password = $password;
}
return $this;
}
And to avoid deprecation messages I've also set this:
#config/packages/framework.yaml
framework:
form:
legacy_error_messages: false
Upvotes: 7
Reputation: 1
I found an answer that works for me, the trick is to use an event listener that listen before the entity is persisted.
So you'll need to create an EventSub like so :
#src/EventSubscriber/EasyAdminSubscriber
<?php
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeEntityPersistedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
namespace App\EventSubscriber;
class EasyAdminSubscriber implements EventSubscriberInterface
{
private $userPasswordHasher;
public function __construct(UserPasswordHasherInterface $userPasswordHasher)
{
$this->userPasswordHasher = $userPasswordHasher;
}
public static function getSubscribedEvents(): array
{
return [
BeforeEntityPersistedEvent::class => ['hashPassWord'],
];
}
public function hashPassWord(BeforeEntityPersistedEvent $event)
{
$entity = $event->getEntityInstance();
if (!($entity instanceof User)) {
return;
}
$entity->setPassword($this->userPasswordHasher->hashPassword($entity, $entity->getPassword()));
}
}
and that's it :)
Upvotes: 0
Reputation: 73
I assumed that it is possible via UserCrudController.
You should overwride methods as:
createEntity(), updateEntity(), persistEntity() and deleteEntity()
Upvotes: 0
Reputation: 103
did you have a solution?
I come across the same issue. My thoughts below
When edit user, have a link to a seperate page / popup where you can change the password.
I check the documentation and i think event subcriber can maybe do the trick to encrypt the password. https://symfony.com/doc/current/bundles/EasyAdminBundle/events.html#event-subscriber-example
Upvotes: 0