Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes are documented here. The project follows [Semantic Versionin
* `runtime.defaultConnection` is always filled in: the configured one, else the first connection. The `propel.dbal.default_connection` parameter the commands use follows the configured one too, and still falls back to the connection named `default` when there is one.
* Autowire the bundle's main classes by type again: `SchemaLocator`, `PropelLogger`, `YamlDataDumper`, `YamlDataLoader` and `XmlDataLoader` resolve to their `propel.*` services.
* Keep the former command service IDs, `propel_bundle_propel.command.*`, as deprecated aliases of the commands.
* The user provider implements `PasswordUpgraderInterface` and delegates password upgrades to the model's Query class when that implements the interface.
* Let the Messenger transport receive several messages per query, as asked by `messenger:consume --fetch-size` in Symfony 8.1+.
* Empty the instance pools of all connections when Symfony resets its services, so Messenger workers and worker runtimes do not keep the objects of a previous message or request.

Expand Down
18 changes: 18 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ public function __construct(
}
```

### User provider

The `propel` user provider loads users through the Query class of the configured model. Symfony rehashes a password on login when the hashing configuration changed, and the provider hands that to the Query class when it implements `PasswordUpgraderInterface`, so the upgrade lives next to the other queries of the model:

```php
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

class UserQuery extends BaseUserQuery implements PasswordUpgraderInterface
{
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
$user->setPassword($newHashedPassword);
$user->save();
}
}
```

### Messenger transport

With [symfony/messenger](https://symfony.com/doc/current/messenger.html) installed, the `propel://` transport stores messages in a table of a Propel connection, the way Symfony's Doctrine transport does. The DSN names the connection:
Expand Down
27 changes: 26 additions & 1 deletion src/Security/User/PropelUserProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
use Propel\Runtime\ActiveRecord\ActiveRecordInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use function get_class;
use function get_debug_type;
use function is_subclass_of;
use function json_encode;
use function sprintf;
Expand All @@ -26,11 +29,13 @@
/**
* Provides easy to use provisioning for Propel model users.
*
* Password upgrades are delegated to the Query class when it implements PasswordUpgraderInterface.
*
* @author William DURAND <william.durand1@gmail.com>
*
* @implements \Symfony\Component\Security\Core\User\UserProviderInterface<\Symfony\Component\Security\Core\User\UserInterface>
*/
class PropelUserProvider implements UserProviderInterface
class PropelUserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
/**
* A Model class name.
Expand Down Expand Up @@ -120,6 +125,26 @@ public function refreshUser(UserInterface $user): UserInterface
return $refreshedUser;
}

/**
* @param \Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface $user
* @param string $newHashedPassword
*
* @throws \Symfony\Component\Security\Core\Exception\UnsupportedUserException
*
* @return void
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof $this->class) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
}

$query = $this->queryClass::create();
if ($query instanceof PasswordUpgraderInterface) {
$query->upgradePassword($user, $newHashedPassword);
}
}

/**
* @param string $class
*
Expand Down
10 changes: 10 additions & 0 deletions tests/Fixtures/Security/PlainUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Propel\Bundle\PropelBundle\Tests\Fixtures\Security;

/**
* A user whose Query class does not upgrade passwords.
*/
class PlainUser extends UpgradableUser
{
}
14 changes: 14 additions & 0 deletions tests/Fixtures/Security/PlainUserQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Propel\Bundle\PropelBundle\Tests\Fixtures\Security;

/**
* Stands in for a generated Query class without password upgrades.
*/
class PlainUserQuery
{
public static function create(): static
{
return new static();
}
}
41 changes: 41 additions & 0 deletions tests/Fixtures/Security/UpgradableUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Propel\Bundle\PropelBundle\Tests\Fixtures\Security;

use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* A user whose Query class upgrades passwords.
*/
class UpgradableUser implements UserInterface, PasswordAuthenticatedUserInterface
{
public function __construct(
private string $password = 'old',
) {
}

public function getRoles(): array
{
return [];
}

public function getUserIdentifier(): string
{
return 'user';
}

public function getPassword(): ?string
{
return $this->password;
}

public function setPassword(string $password): void
{
$this->password = $password;
}

public function eraseCredentials(): void
{
}
}
31 changes: 31 additions & 0 deletions tests/Fixtures/Security/UpgradableUserQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Propel\Bundle\PropelBundle\Tests\Fixtures\Security;

use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

/**
* Stands in for a generated Query class that implements the password upgrade.
*/
class UpgradableUserQuery implements PasswordUpgraderInterface
{
/**
* @var list<array{PasswordAuthenticatedUserInterface, string}>
*/
public static array $upgrades = [];

public static function create(): static
{
return new static();
}

public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
self::$upgrades[] = [$user, $newHashedPassword];

if ($user instanceof UpgradableUser) {
$user->setPassword($newHashedPassword);
}
}
}
39 changes: 39 additions & 0 deletions tests/Security/User/PropelUserProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@

use Propel\Bundle\PropelBundle\Security\User\PropelUserProvider;
use Propel\Bundle\PropelBundle\Tests\Fixtures\Model\User;
use Propel\Bundle\PropelBundle\Tests\Fixtures\Security\PlainUser;
use Propel\Bundle\PropelBundle\Tests\Fixtures\Security\UpgradableUser;
use Propel\Bundle\PropelBundle\Tests\Fixtures\Security\UpgradableUserQuery;
use Propel\Bundle\PropelBundle\Tests\TestCase;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;

/**
* @author William Durand <william.durand1@gmail.com>
Expand Down Expand Up @@ -107,4 +112,38 @@ public function testLoadUserByUsernameIsDeprecated() {

$this->assertInstanceOf(User::class, $provider->loadUserByUsername('user3'));
}

public function testUpgradePasswordDelegatesToTheQueryClass()
{
UpgradableUserQuery::$upgrades = [];
$user = new UpgradableUser('old');
$provider = new PropelUserProvider(UpgradableUser::class);

$this->assertInstanceOf(PasswordUpgraderInterface::class, $provider);

$provider->upgradePassword($user, 'new');

$this->assertSame([[$user, 'new']], UpgradableUserQuery::$upgrades);
$this->assertSame('new', $user->getPassword());
}

public function testUpgradePasswordDoesNothingWithoutAnUpgradingQueryClass()
{
$user = new PlainUser('old');
$provider = new PropelUserProvider(PlainUser::class);

$provider->upgradePassword($user, 'new');

$this->assertSame('old', $user->getPassword());
}

public function testUpgradePasswordRejectsOtherUsers()
{
$provider = new PropelUserProvider(PlainUser::class);

$this->expectException(UnsupportedUserException::class);
$this->expectExceptionMessage('Instances of "Propel\Bundle\PropelBundle\Tests\Fixtures\Security\UpgradableUser" are not supported.');

$provider->upgradePassword(new UpgradableUser(), 'new');
}
}
Loading