From 42d4d1133916ea1101872665cd3c13d2ea18175f Mon Sep 17 00:00:00 2001 From: Ambroise Maupate Date: Thu, 3 Aug 2023 15:23:51 +0200 Subject: [PATCH 1/2] feat(Security): Added UserChecker to check users enabled, expired, locked or credentialExpired. Removed useless User' boolean expired and credentialsExpired fields. --- config/packages/security.yaml | 2 + .../Authentication/OpenIdAuthenticator.php | 3 - .../config/packages/security.yaml | 2 + .../migrations/Version20230803131631.php | 57 ++++++++ .../src/Console/UsersCommand.php | 60 +++++--- .../src/Console/UsersExpireCommand.php | 59 ++++++++ .../src/Console/UsersLockCommand.php | 51 +++++++ .../src/Console/UsersUnexpireCommand.php | 51 +++++++ .../src/Console/UsersUnlockCommand.php | 51 +++++++ .../UserLifeCycleSubscriber.php | 2 +- lib/RoadizCoreBundle/src/Entity/User.php | 137 ++++-------------- .../UserCredentialsExpiredException.php | 15 ++ .../Exception/UserExpiredException.php | 15 ++ .../Exception/UserLockedException.php | 15 ++ .../Exception/UserNotEnabledException.php | 15 ++ .../src/Security/UserChecker.php | 48 ++++++ .../src/Traits/LoginResetTrait.php | 3 - .../translations/security.en.xlf | 23 +++ .../translations/security.fr.xlf | 23 +++ .../translations/security.xlf | 23 +++ .../RoadizRozierExtension.php | 1 - lib/Rozier/src/Forms/UserSecurityType.php | 8 - 22 files changed, 521 insertions(+), 143 deletions(-) create mode 100644 lib/RoadizCoreBundle/migrations/Version20230803131631.php create mode 100644 lib/RoadizCoreBundle/src/Console/UsersExpireCommand.php create mode 100644 lib/RoadizCoreBundle/src/Console/UsersLockCommand.php create mode 100644 lib/RoadizCoreBundle/src/Console/UsersUnexpireCommand.php create mode 100644 lib/RoadizCoreBundle/src/Console/UsersUnlockCommand.php create mode 100644 lib/RoadizCoreBundle/src/Security/Exception/UserCredentialsExpiredException.php create mode 100644 lib/RoadizCoreBundle/src/Security/Exception/UserExpiredException.php create mode 100644 lib/RoadizCoreBundle/src/Security/Exception/UserLockedException.php create mode 100644 lib/RoadizCoreBundle/src/Security/Exception/UserNotEnabledException.php create mode 100644 lib/RoadizCoreBundle/src/Security/UserChecker.php create mode 100644 lib/RoadizCoreBundle/translations/security.en.xlf create mode 100644 lib/RoadizCoreBundle/translations/security.fr.xlf create mode 100644 lib/RoadizCoreBundle/translations/security.xlf diff --git a/config/packages/security.yaml b/config/packages/security.yaml index a7cfdd61..dc680ea9 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -36,6 +36,7 @@ security: success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure jwt: ~ + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker # disables session creation for assets and healthcheck controllers assets: pattern: ^/assets @@ -48,6 +49,7 @@ security: main: lazy: true provider: all_users + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker # activate different ways to authenticate # https://symfony.com/doc/current/security.html#firewalls-authentication diff --git a/lib/OpenId/src/Authentication/OpenIdAuthenticator.php b/lib/OpenId/src/Authentication/OpenIdAuthenticator.php index 320b9786..3d79012f 100644 --- a/lib/OpenId/src/Authentication/OpenIdAuthenticator.php +++ b/lib/OpenId/src/Authentication/OpenIdAuthenticator.php @@ -50,7 +50,6 @@ final class OpenIdAuthenticator extends AbstractAuthenticator private string $targetPathParameter; private array $defaultRoles; private bool $forceSsl; - private UserProviderInterface $accountProvider; private bool $requiresLocalUsers; public function __construct( @@ -59,7 +58,6 @@ public function __construct( JwtRoleStrategy $roleStrategy, OpenIdJwtConfigurationFactory $jwtConfigurationFactory, UrlGeneratorInterface $urlGenerator, - UserProviderInterface $accountProvider, string $returnPath, string $defaultRoute, ?string $oauthClientId, @@ -87,7 +85,6 @@ public function __construct( $this->urlGenerator = $urlGenerator; $this->jwtConfigurationFactory = $jwtConfigurationFactory; $this->forceSsl = $forceSsl; - $this->accountProvider = $accountProvider; $this->requiresLocalUsers = $requiresLocalUsers; } diff --git a/lib/RoadizCoreBundle/config/packages/security.yaml b/lib/RoadizCoreBundle/config/packages/security.yaml index 00ef8080..4c2ce00a 100644 --- a/lib/RoadizCoreBundle/config/packages/security.yaml +++ b/lib/RoadizCoreBundle/config/packages/security.yaml @@ -25,6 +25,7 @@ security: pattern: ^/api stateless: true provider: all_users + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker login_throttling: max_attempts: 3 json_login: @@ -46,6 +47,7 @@ security: main: lazy: true provider: all_users + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker # activate different ways to authenticate # https://symfony.com/doc/current/security.html#firewalls-authentication diff --git a/lib/RoadizCoreBundle/migrations/Version20230803131631.php b/lib/RoadizCoreBundle/migrations/Version20230803131631.php new file mode 100644 index 00000000..8367dd34 --- /dev/null +++ b/lib/RoadizCoreBundle/migrations/Version20230803131631.php @@ -0,0 +1,57 @@ +addSql('DROP INDEX IDX_1483A5E9194FED4B ON users'); + $this->addSql('ALTER TABLE users DROP expired, DROP credentials_expired'); + $this->addSql('CREATE INDEX idx_users_username ON users (username)'); + $this->addSql('CREATE INDEX idx_users_email ON users (email)'); + $this->addSql('CREATE INDEX idx_users_credentials_expires_at ON users (credentials_expires_at)'); + $this->addSql('CREATE INDEX idx_users_password_requested_at ON users (password_requested_at)'); + $this->addSql('CREATE INDEX idx_users_last_login ON users (last_login)'); + $this->addSql('CREATE INDEX idx_users_locked ON users (locked)'); + $this->addSql('CREATE INDEX IDX_1483A5E98B8E8428 ON users (created_at)'); + $this->addSql('CREATE INDEX IDX_1483A5E943625D9F ON users (updated_at)'); + $this->addSql('ALTER TABLE users RENAME INDEX idx_1483a5e950f9bb84 TO idx_users_enabled'); + $this->addSql('ALTER TABLE users RENAME INDEX idx_1483a5e9f9d83e2 TO idx_users_expires_at'); + $this->addSql('ALTER TABLE users RENAME INDEX idx_1483a5e94180c698 TO idx_users_locale'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX idx_users_username ON users'); + $this->addSql('DROP INDEX idx_users_email ON users'); + $this->addSql('DROP INDEX idx_users_credentials_expires_at ON users'); + $this->addSql('DROP INDEX idx_users_password_requested_at ON users'); + $this->addSql('DROP INDEX idx_users_last_login ON users'); + $this->addSql('DROP INDEX idx_users_locked ON users'); + $this->addSql('DROP INDEX IDX_1483A5E98B8E8428 ON users'); + $this->addSql('DROP INDEX IDX_1483A5E943625D9F ON users'); + $this->addSql('ALTER TABLE users ADD expired TINYINT(1) DEFAULT 0 NOT NULL, ADD credentials_expired TINYINT(1) DEFAULT 0 NOT NULL'); + $this->addSql('CREATE INDEX IDX_1483A5E9194FED4B ON users (expired)'); + $this->addSql('ALTER TABLE users RENAME INDEX idx_users_locale TO IDX_1483A5E94180C698'); + $this->addSql('ALTER TABLE users RENAME INDEX idx_users_enabled TO IDX_1483A5E950F9BB84'); + $this->addSql('ALTER TABLE users RENAME INDEX idx_users_expires_at TO IDX_1483A5E9F9D83E2'); + } + + public function isTransactional(): bool + { + return false; + } +} diff --git a/lib/RoadizCoreBundle/src/Console/UsersCommand.php b/lib/RoadizCoreBundle/src/Console/UsersCommand.php index 56ffa617..f86c7100 100644 --- a/lib/RoadizCoreBundle/src/Console/UsersCommand.php +++ b/lib/RoadizCoreBundle/src/Console/UsersCommand.php @@ -8,6 +8,7 @@ use RZ\Roadiz\CoreBundle\Entity\Role; use RZ\Roadiz\CoreBundle\Entity\User; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -40,6 +41,19 @@ protected function configure(): void ); } + protected function getUserTableRow(User $user): array + { + return [ + 'Id' => $user->getId(), + 'Username' => $user->getUsername(), + 'Email' => $user->getEmail(), + 'Disabled' => (!$user->isEnabled() ? 'X' : ''), + 'Expired' => (!$user->isAccountNonExpired() ? 'X' : ''), + 'Locked' => (!$user->isAccountNonLocked() ? 'X' : ''), + 'Groups' => implode(' ', $user->getGroupNames()), + ]; + } + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -54,18 +68,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($user === null) { $io->error('User “' . $name . '” does not exist… use users:create to add a new user.'); } else { - $tableContent = [[ - $user->getId(), - $user->getUsername(), - $user->getEmail(), - (!$user->isEnabled() ? 'X' : ''), - ($user->getExpired() ? 'X' : ''), - (!$user->isAccountNonLocked() ? 'X' : ''), - implode(' ', $user->getGroupNames()), - implode(' ', $user->getRoles()), - ]]; + $tableContent = [$this->getUserTableRow($user)]; $io->table( - ['Id', 'Username', 'Email', 'Disabled', 'Expired', 'Locked', 'Groups', 'Roles'], + array_keys($tableContent[0]), $tableContent ); } @@ -77,20 +82,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (count($users) > 0) { $tableContent = []; foreach ($users as $user) { - $tableContent[] = [ - $user->getId(), - $user->getUsername(), - $user->getEmail(), - (!$user->isEnabled() ? 'X' : ''), - ($user->getExpired() ? 'X' : ''), - (!$user->isAccountNonLocked() ? 'X' : ''), - implode(' ', $user->getGroupNames()), - implode(' ', $user->getRoles()), - ]; + $tableContent[] = $this->getUserTableRow($user); } $io->table( - ['Id', 'Username', 'Email', 'Disabled', 'Expired', 'Locked', 'Groups', 'Roles'], + array_keys($tableContent[0]), $tableContent ); } else { @@ -100,6 +96,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + protected function getUserForInput(InputInterface $input): User + { + $name = $input->getArgument('username'); + + if (!\is_string($name) || empty($name)) { + throw new InvalidArgumentException('Username argument is required.'); + } + + /** @var User|null $user */ + $user = $this->managerRegistry + ->getRepository(User::class) + ->findOneBy(['username' => $name]); + + if (!($user instanceof User)) { + throw new InvalidArgumentException('User “' . $name . '” does not exist.'); + } + + return $user; + } + /** * Get role by name, and create it if it does not exist. * diff --git a/lib/RoadizCoreBundle/src/Console/UsersExpireCommand.php b/lib/RoadizCoreBundle/src/Console/UsersExpireCommand.php new file mode 100644 index 00000000..f8aafd30 --- /dev/null +++ b/lib/RoadizCoreBundle/src/Console/UsersExpireCommand.php @@ -0,0 +1,59 @@ +setName('users:expire') + ->setDescription('Set a user account expiration date') + ->addArgument( + 'username', + InputArgument::REQUIRED, + 'Username' + ) + ->addArgument( + 'expiry', + InputArgument::OPTIONAL, + 'Expiration date and time (Y-m-d H:i:s)' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); + $expirationDate = new \DateTime($input->getArgument('expiry') ?? 'now'); + + $question = sprintf( + 'Do you really want to set user “%s” expiration date on %s?', + $user->getUsername(), + $expirationDate->format('c') + ); + $confirmation = new ConfirmationQuestion($question, false); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setExpiresAt($expirationDate); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” expiration date was set on ' . $expirationDate->format('c') . '.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not updated.'); + return 1; + } + } +} diff --git a/lib/RoadizCoreBundle/src/Console/UsersLockCommand.php b/lib/RoadizCoreBundle/src/Console/UsersLockCommand.php new file mode 100644 index 00000000..03e9b9dc --- /dev/null +++ b/lib/RoadizCoreBundle/src/Console/UsersLockCommand.php @@ -0,0 +1,51 @@ +setName('users:lock') + ->setDescription('Lock a user account') + ->addArgument( + 'username', + InputArgument::REQUIRED, + 'Username' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); + + $confirmation = new ConfirmationQuestion( + 'Do you really want to lock user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setLocked(true); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” locked.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not locked.'); + return 1; + } + } +} diff --git a/lib/RoadizCoreBundle/src/Console/UsersUnexpireCommand.php b/lib/RoadizCoreBundle/src/Console/UsersUnexpireCommand.php new file mode 100644 index 00000000..0430a728 --- /dev/null +++ b/lib/RoadizCoreBundle/src/Console/UsersUnexpireCommand.php @@ -0,0 +1,51 @@ +setName('users:unexpire') + ->setDescription('Remove a user account expiration date') + ->addArgument( + 'username', + InputArgument::REQUIRED, + 'Username' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); + + $confirmation = new ConfirmationQuestion( + 'Do you really want to remove user “' . $user->getUsername() . '” expiration date?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setExpiresAt(null); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” unexpired.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not updated.'); + return 1; + } + } +} diff --git a/lib/RoadizCoreBundle/src/Console/UsersUnlockCommand.php b/lib/RoadizCoreBundle/src/Console/UsersUnlockCommand.php new file mode 100644 index 00000000..0fd4be6a --- /dev/null +++ b/lib/RoadizCoreBundle/src/Console/UsersUnlockCommand.php @@ -0,0 +1,51 @@ +setName('users:unlock') + ->setDescription('Unlock a user account') + ->addArgument( + 'username', + InputArgument::REQUIRED, + 'Username' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $name = $input->getArgument('username'); + $user = $this->getUserForInput($input); + + $confirmation = new ConfirmationQuestion( + 'Do you really want to unlock user “' . $user->getUsername() . '”?', + false + ); + if ( + !$input->isInteractive() || $io->askQuestion( + $confirmation + ) + ) { + $user->setLocked(false); + $this->managerRegistry->getManagerForClass(User::class)->flush(); + $io->success('User “' . $name . '” unlocked.'); + return 0; + } else { + $io->warning('User “' . $name . '” was not unlocked.'); + return 1; + } + } +} diff --git a/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php b/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php index f6183e1b..eb342cdb 100644 --- a/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php +++ b/lib/RoadizCoreBundle/src/Doctrine/EventSubscriber/UserLifeCycleSubscriber.php @@ -185,7 +185,7 @@ public function prePersist(LifecycleEventArgs $event): void * just send them a password reset link. */ $tokenGenerator = new TokenGenerator($this->logger); - $user->setCredentialsExpired(true); + $user->setCredentialsExpiresAt(new \DateTime('-1 day')); $user->setPasswordRequestedAt(new \DateTime()); $user->setConfirmationToken($tokenGenerator->generateToken()); diff --git a/lib/RoadizCoreBundle/src/Entity/User.php b/lib/RoadizCoreBundle/src/Entity/User.php index aa346f1b..092eed88 100644 --- a/lib/RoadizCoreBundle/src/Entity/User.php +++ b/lib/RoadizCoreBundle/src/Entity/User.php @@ -24,10 +24,15 @@ #[ ORM\Entity(repositoryClass: UserRepository::class), ORM\Table(name: "users"), - ORM\Index(columns: ["enabled"]), - ORM\Index(columns: ["expired"]), - ORM\Index(columns: ["expires_at"]), - ORM\Index(columns: ["locale"]), + ORM\Index(columns: ["username"], name: "idx_users_username"), + ORM\Index(columns: ["email"], name: "idx_users_email"), + ORM\Index(columns: ["enabled"], name: "idx_users_enabled"), + ORM\Index(columns: ["credentials_expires_at"], name: "idx_users_credentials_expires_at"), + ORM\Index(columns: ["password_requested_at"], name: "idx_users_password_requested_at"), + ORM\Index(columns: ["expires_at"], name: "idx_users_expires_at"), + ORM\Index(columns: ["last_login"], name: "idx_users_last_login"), + ORM\Index(columns: ["locked"], name: "idx_users_locked"), + ORM\Index(columns: ["locale"], name: "idx_users_locale"), ORM\HasLifecycleCallbacks, UniqueEntity("email"), UniqueEntity("username") @@ -180,14 +185,6 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface #[SymfonySerializer\Groups(['user_group'])] private Collection $groups; - /** - * @var boolean - * @Serializer\Groups({"user_security"}) - */ - #[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])] - #[SymfonySerializer\Groups(['user_security'])] - private bool $expired = false; - /** * @var boolean * @Serializer\Groups({"user_security"}) @@ -204,14 +201,6 @@ class User extends AbstractHuman implements UserInterface, AdvancedUserInterface #[SymfonySerializer\Groups(['user_security'])] private ?\DateTime $credentialsExpiresAt = null; - /** - * @var boolean - * @Serializer\Groups({"user_security"}) - */ - #[ORM\Column(name: 'credentials_expired', type: 'boolean', nullable: false, options: ['default' => false])] - #[SymfonySerializer\Groups(['user_security'])] - private bool $credentialsExpired = false; - /** * @Serializer\Groups({"user_security"}) * @var \DateTime|null @@ -421,7 +410,7 @@ public function setPlainPassword(?string $plainPassword): User } /** - * @return \DateTime $lastLogin + * @return \DateTime|null $lastLogin */ public function getLastLogin(): ?\DateTime { @@ -441,7 +430,7 @@ public function setLastLogin(?\DateTime $lastLogin): User /** * Get random string sent to the user email address in order to verify it. * - * @return string + * @return string|null */ public function getConfirmationToken(): ?string { @@ -525,7 +514,7 @@ public function addRoleEntity(Role $role): User /** * Get roles entities * - * @return Collection + * @return Collection|null */ public function getRolesEntities(): ?Collection { @@ -635,27 +624,6 @@ public function getGroupNames(): array return $names; } - /** - * Return strictly forced expiration status. - * - * @return boolean - */ - public function getExpired(): bool - { - return $this->expired; - } - - /** - * @param boolean $expired - * @return $this - */ - public function setExpired(bool $expired): User - { - $this->expired = $expired; - - return $this; - } - /** * Checks whether the user's account has expired. * @@ -673,14 +641,7 @@ public function setExpired(bool $expired): User #[SymfonySerializer\Groups(['user_security'])] public function isAccountNonExpired(): bool { - if ( - $this->expiresAt !== null && - $this->expiresAt->getTimestamp() < time() - ) { - return false; - } - - return !$this->expired; + return $this->expiresAt === null || $this->expiresAt->getTimestamp() > time(); } /** @@ -729,7 +690,7 @@ public function equals(User $user): bool } /** - * @return \DateTime + * @return \DateTime|null */ public function getCredentialsExpiresAt(): ?\DateTime { @@ -748,47 +709,6 @@ public function setCredentialsExpiresAt(?\DateTime $date = null): User return $this; } - /** - * Return strictly forced credential expiration status. - * - * @return boolean - */ - public function getCredentialsExpired(): bool - { - return $this->credentialsExpired; - } - - /** - * @param boolean $credentialsExpired - * @return $this - */ - public function setCredentialsExpired(bool $credentialsExpired): User - { - $this->credentialsExpired = $credentialsExpired; - - return $this; - } - - /** - * @return \DateTime - */ - public function getExpiresAt(): ?\DateTime - { - return $this->expiresAt; - } - - /** - * @param \DateTime|null $date - * - * @return User - */ - public function setExpiresAt(?\DateTime $date = null): User - { - $this->expiresAt = $date; - - return $this; - } - /** * @return Node|null * @internal Do use directly, use NodeChrootResolver class to support external users (SSO, oauth2, …) @@ -876,14 +796,25 @@ public function setEnabled(bool $enabled): User #[SymfonySerializer\Ignore] public function isCredentialsNonExpired(): bool { - if ( - $this->credentialsExpiresAt !== null && - $this->credentialsExpiresAt->getTimestamp() < time() - ) { - return false; - } + return $this->credentialsExpiresAt === null || $this->credentialsExpiresAt->getTimestamp() > time(); + } + + /** + * @return \DateTime|null + */ + public function getExpiresAt(): ?\DateTime + { + return $this->expiresAt; + } - return !$this->credentialsExpired; + /** + * @param \DateTime|null $expiresAt + * @return User + */ + public function setExpiresAt(?\DateTime $expiresAt): User + { + $this->expiresAt = $expiresAt; + return $this; } /** @@ -952,10 +883,8 @@ public function __serialize(): array $this->roleEntities, $this->groups, // needed for advancedUserinterface - $this->expired, $this->expiresAt, $this->locked, - $this->credentialsExpired, $this->credentialsExpiresAt, ]; } @@ -971,10 +900,8 @@ public function __unserialize(array $data): void $this->email, $this->roleEntities, $this->groups, - $this->expired, $this->expiresAt, $this->locked, - $this->credentialsExpired, $this->credentialsExpiresAt, ] = $data; } diff --git a/lib/RoadizCoreBundle/src/Security/Exception/UserCredentialsExpiredException.php b/lib/RoadizCoreBundle/src/Security/Exception/UserCredentialsExpiredException.php new file mode 100644 index 00000000..58f9fa05 --- /dev/null +++ b/lib/RoadizCoreBundle/src/Security/Exception/UserCredentialsExpiredException.php @@ -0,0 +1,15 @@ +isEnabled()) { + // the message passed to this exception is meant to be displayed to the user + throw new UserNotEnabledException('Your user account is not enabled. Contact an administrator.'); + } + } + + public function checkPostAuth(UserInterface $user): void + { + if (!$user instanceof User) { + return; + } + + if (!$user->isAccountNonLocked()) { + // the message passed to this exception is meant to be displayed to the user + throw new UserLockedException('Your user account is locked. Contact an administrator.'); + } + if (!$user->isCredentialsNonExpired()) { + // the message passed to this exception is meant to be displayed to the user + throw new UserCredentialsExpiredException('Your credentials have expired. Please request a new password.'); + } + if (!$user->isAccountNonExpired()) { + // the message passed to this exception is meant to be displayed to the user + throw new UserExpiredException('Your account has expired. Contact an administrator.'); + } + } +} diff --git a/lib/RoadizCoreBundle/src/Traits/LoginResetTrait.php b/lib/RoadizCoreBundle/src/Traits/LoginResetTrait.php index 2788cb3a..08a3e7e3 100644 --- a/lib/RoadizCoreBundle/src/Traits/LoginResetTrait.php +++ b/lib/RoadizCoreBundle/src/Traits/LoginResetTrait.php @@ -37,9 +37,6 @@ public function updateUserPassword(FormInterface $form, User $user, ObjectManage * we remove expiration. */ if (!$user->isCredentialsNonExpired()) { - if ($user->getCredentialsExpired() === true) { - $user->setCredentialsExpired(false); - } if (null !== $user->getCredentialsExpiresAt()) { $user->setCredentialsExpiresAt(null); } diff --git a/lib/RoadizCoreBundle/translations/security.en.xlf b/lib/RoadizCoreBundle/translations/security.en.xlf new file mode 100644 index 00000000..76189574 --- /dev/null +++ b/lib/RoadizCoreBundle/translations/security.en.xlf @@ -0,0 +1,23 @@ + + + + + + Your user account is not enabled. Contact an administrator. + Your user account is not enabled. Contact an administrator. + + + Your credentials have expired. Please request a new password. + Your credentials have expired. Please request a new password. + + + Your account has expired. Contact an administrator. + Your account has expired. Contact an administrator. + + + Your user account is locked. Contact an administrator. + Your user account is locked. Contact an administrator. + + + + diff --git a/lib/RoadizCoreBundle/translations/security.fr.xlf b/lib/RoadizCoreBundle/translations/security.fr.xlf new file mode 100644 index 00000000..1b6f7a74 --- /dev/null +++ b/lib/RoadizCoreBundle/translations/security.fr.xlf @@ -0,0 +1,23 @@ + + + + + + Your user account is not enabled. Contact an administrator. + Votre compte utilisateur n'est pas activé. Contactez un administrateur. + + + Your credentials have expired. Please request a new password. + Vos informations d'identification ont expiré. Veuillez demander un nouveau mot de passe. + + + Your account has expired. Contact an administrator. + Votre compte a expiré. Contactez un administrateur. + + + Your user account is locked. Contact an administrator. + Votre compte utilisateur est verrouillé. Contactez un administrateur. + + + + diff --git a/lib/RoadizCoreBundle/translations/security.xlf b/lib/RoadizCoreBundle/translations/security.xlf new file mode 100644 index 00000000..20fe3251 --- /dev/null +++ b/lib/RoadizCoreBundle/translations/security.xlf @@ -0,0 +1,23 @@ + + + + + + Your user account is not enabled. Contact an administrator. + + + + Your credentials have expired. Please request a new password. + + + + Your account has expired. Contact an administrator. + + + + Your user account is locked. Contact an administrator. + + + + + diff --git a/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php b/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php index bfbe3a13..5d25ceac 100644 --- a/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php +++ b/lib/RoadizRozierBundle/src/DependencyInjection/RoadizRozierExtension.php @@ -93,7 +93,6 @@ private function registerOpenId(array $config, ContainerBuilder $container): voi new Reference(\RZ\Roadiz\OpenId\Authentication\Provider\ChainJwtRoleStrategy::class), new Reference('roadiz_rozier.open_id.jwt_configuration_factory'), new Reference(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class), - new Reference(\RZ\Roadiz\OpenId\Authentication\Provider\OpenIdAccountProvider::class), 'loginPage', 'adminHomePage', $config['open_id']['oauth_client_id'], diff --git a/lib/Rozier/src/Forms/UserSecurityType.php b/lib/Rozier/src/Forms/UserSecurityType.php index dd14825b..3055d7d1 100644 --- a/lib/Rozier/src/Forms/UserSecurityType.php +++ b/lib/Rozier/src/Forms/UserSecurityType.php @@ -38,10 +38,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'minute' => 'minute', ], ]) - ->add('expired', CheckboxType::class, [ - 'label' => 'user.force.expired', - 'required' => false, - ]) ->add('credentialsExpiresAt', DateTimeType::class, [ 'label' => 'user.credentialsExpiresAt', 'required' => false, @@ -55,10 +51,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'hour' => 'hour', 'minute' => 'minute', ], - ]) - ->add('credentialsExpired', CheckboxType::class, [ - 'label' => 'user.force.credentialsExpired', - 'required' => false, ]); if ($options['canChroot'] === true) { From be2e19a8004b114c0bbbb1124c37dd73b78f0985 Mon Sep 17 00:00:00 2001 From: Ambroise Maupate Date: Thu, 3 Aug 2023 15:32:21 +0200 Subject: [PATCH 2/2] chore: Bumped --- CHANGELOG.md | 20 ++++++++++++++++++++ lib/RoadizCoreBundle/config/services.yaml | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3df2c3..b6b89579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [v2.1.37](https://github.com/roadiz/core-bundle-dev-app/compare/v2.1.36...v2.1.37) (2023-08-03) + + +### Features + +* New Doctrine migration +* **Security:** Added UserChecker to check users enabled, expired, locked or credentialExpired. Removed useless User' boolean expired and credentialsExpired fields. ([42d4d11](https://github.com/roadiz/core-bundle-dev-app/commit/42d4d1133916ea1101872665cd3c13d2ea18175f)) + - Make sure to register Roadiz `UserChecker` in your `security.yaml` file: https://symfony.com/doc/current/security/user_checkers.html#enabling-the-custom-user-checker + +```yaml +# config/packages/security.yaml + +# ... +security: + firewalls: + main: + pattern: ^/ + user_checker: RZ\Roadiz\CoreBundle\Security\UserChecker +``` + ## [v2.1.36](https://github.com/roadiz/core-bundle-dev-app/compare/v2.1.35...v2.1.36) (2023-08-03) diff --git a/lib/RoadizCoreBundle/config/services.yaml b/lib/RoadizCoreBundle/config/services.yaml index 9346b1c3..c534e6a4 100644 --- a/lib/RoadizCoreBundle/config/services.yaml +++ b/lib/RoadizCoreBundle/config/services.yaml @@ -1,6 +1,6 @@ --- parameters: - roadiz_core.cms_version: '2.1.36' + roadiz_core.cms_version: '2.1.37' roadiz_core.cms_version_prefix: 'main' env(APP_NAMESPACE): "roadiz" env(APP_VERSION): "0.1.0"