Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Controller\Api;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\Permission;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class UserApiControllerFunctionalTest extends MauticMysqlTestCase
{
public function testRoleUpdateByApiGivesErrorResponseIfUserDoesNotExist(): void
{
// Assuming user with id 99999 does not exist
$this->client->request(Request::METHOD_PATCH, '/api/users/99999/edit', ['role' => 1]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_NOT_FOUND, $clientResponse->getStatusCode());
Assert::assertStringContainsString('"message":"Item was not found."', $clientResponse->getContent());
}
public function testRoleUpdateByApiGivesErrorResponseIfRoleDoesNotExist(): void
{
// Assuming role with id 99999 does not exist
$this->client->request(Request::METHOD_PATCH, '/api/users/1/edit', ['role' => 99999]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_BAD_REQUEST, $clientResponse->getStatusCode());
Assert::assertStringContainsString('"message":"role: The selected choice is invalid."', $clientResponse->getContent());
}
public function testRoleUpdateByApiGivesErrorResponseWithInvalidRequestFormat(): void
{
// Correct request format is ['role' => 2]
$this->client->request(Request::METHOD_PATCH, '/api/users/1/edit', ['role' => ['id' => 2]]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_BAD_REQUEST, $clientResponse->getStatusCode());
Assert::assertStringContainsString('"message":"role: The selected choice is invalid."', $clientResponse->getContent());
}
public function testRoleUpdateByApiGivesErrorResponseIfUserDoesNotHaveValidPermissionToUpdate(): void
{
// Create non-admin role
$role = $this->createRole();
// Create permissions for the role
$this->createPermission('lead:leads:viewown', $role, 1024);
// Create non-admin user
$user = $this->createUser($role);
$this->em->flush();
$this->em->clear();
// Login newly created non-admin user
$this->loginUser($user);
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
$this->client->request(Request::METHOD_PATCH, "/api/users/{$user->getId()}/edit", ['role' => $role->getId()]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_FORBIDDEN, $clientResponse->getStatusCode());
Assert::assertStringContainsString(
'"message":"You do not have access to the requested area\/action."',
$clientResponse->getContent()
);
}
public function testRoleUpdateByApiThroughAdminUserGivesSuccessResponse(): void
{
// Create admin role
$role = $this->createRole(true);
// Create admin user
$user = $this->createUser($role);
$this->em->flush();
$this->em->clear();
// Login newly created admin user
$this->loginUser($user);
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
$this->client->request(Request::METHOD_PATCH, "/api/users/{$user->getId()}/edit", ['role' => $role->getId()]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
Assert::assertStringContainsString('"username":"'.$user->getUserIdentifier().'"', $clientResponse->getContent());
}
public function testRoleUpdateByApiThroughNonAdminUserGivesSuccessResponse(): void
{
// Create non-admin role
$role = $this->createRole();
// Create permissions to update user for the role
$this->createPermission('user:users:edit', $role, 52);
// Create non-admin user
$user = $this->createUser($role);
$this->em->flush();
$this->em->clear();
$this->loginUser($user);
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
$this->client->request(Request::METHOD_PATCH, "/api/users/{$user->getId()}/edit", ['role' => $role->getId()]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
Assert::assertStringContainsString('"username":"'.$user->getUserIdentifier().'"', $clientResponse->getContent());
}
public function testWeakPasswordGivesUnauthorizedResponse(): void
{
// Create non-admin role
$role = $this->createRole();
// Create permissions to update user for the role
$this->createPermission('user:users:edit', $role, 52);
// Create non-admin user with weak password.
$weakPassword = 'mautic';
$user = $this->createUser($role, $weakPassword);
$this->em->flush();
$this->em->clear();
$this->loginUser($user);
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
$this->client->setServerParameter('PHP_AUTH_PW', $weakPassword);
$this->client->request(Request::METHOD_PATCH, "/api/users/{$user->getId()}/edit", ['role' => $role->getId()]);
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_UNAUTHORIZED, $clientResponse->getStatusCode());
}
#[\PHPUnit\Framework\Attributes\DataProvider('passwordProvider')]
public function testUserPasswordPolicy(int $responseCode, string $password): void
{
$userPayload = [
'username' => 'lorem_ipsum',
'firstName' => 'lorem',
'lastName' => 'ipsum',
'email' => 'loremipsum@example.com',
'plainPassword' => ['password' => $password, 'confirm' => $password],
'role' => 1,
];
$this->client->request(Request::METHOD_POST, '/api/users/new', $userPayload);
$clientResponse = $this->client->getResponse();
Assert::assertSame($responseCode, $clientResponse->getStatusCode());
}
/**
* @return iterable<array<int, mixed>>
*/
public static function passwordProvider(): iterable
{
yield [Response::HTTP_BAD_REQUEST, 'aaa'];
yield [Response::HTTP_BAD_REQUEST, 'qwerty'];
yield [Response::HTTP_BAD_REQUEST, 'qwerty123'];
yield [Response::HTTP_CREATED, 'Qwertee@123'];
}
private function createRole(bool $isAdmin = false): Role
{
$role = new Role();
$role->setName('Role');
$role->setIsAdmin($isAdmin);
$this->em->persist($role);
return $role;
}
private function createPermission(string $rawPermission, Role $role, int $bitwise): void
{
$parts = explode(':', $rawPermission);
$permission = new Permission();
$permission->setBundle($parts[0]);
$permission->setName($parts[1]);
$permission->setRole($role);
$permission->setBitwise($bitwise);
$this->em->persist($permission);
}
private function createUser(Role $role, string $password = 'Maut1cR0cks!'): User
{
$user = new User();
$user->setFirstName('John');
$user->setLastName('Doe');
$user->setUsername('john.doe');
$user->setEmail('john.doe@email.com');
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
\assert($hasher instanceof PasswordHasherInterface);
$user->setPassword($hasher->hash($password));
$user->setRole($role);
$this->em->persist($user);
return $user;
}
/**
* Test creating a user via API Platform v2 endpoint.
*
* @param array<string, mixed> $userData
*/
#[\PHPUnit\Framework\Attributes\DataProvider('userCreateDataProvider')]
public function testCreateUserViaApiPlatform(array $userData, int $expectedStatusCode): void
{
// Create a role first
$role = new Role();
$role->setName('Test Role');
$role->setDescription('Test role for API');
$this->em->persist($role);
$this->em->flush();
// Set the role IRI in the user data
$userData['role'] = sprintf('/api/v2/roles/%d', $role->getId());
$this->client->request(
'POST',
'/api/v2/users',
[],
[],
[
'CONTENT_TYPE' => 'application/ld+json',
'HTTP_ACCEPT' => 'application/ld+json',
],
json_encode($userData)
);
$response = $this->client->getResponse();
$this->assertResponseIsSuccessful();
if (Response::HTTP_CREATED === $expectedStatusCode) {
$responseData = json_decode($response->getContent(), true);
$this->assertIsArray($responseData);
$this->assertArrayHasKey('id', $responseData);
$this->assertArrayHasKey('username', $responseData);
// Verify the user was actually created in the database
$userRepository = $this->em->getRepository(User::class);
$user = $userRepository->find($responseData['id']);
$this->assertInstanceOf(User::class, $user);
$this->assertSame($userData['username'], $user->getUsername());
$this->assertSame($userData['firstName'], $user->getFirstName());
$this->assertSame($userData['lastName'], $user->getLastName());
$this->assertSame($userData['email'], $user->getEmail());
// Verify the password was hashed correctly by checking if we can verify it
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
\assert($hasher instanceof PasswordHasherInterface);
$this->assertTrue(
$hasher->verify($user->getPassword(), $userData['plainPassword']),
'Password should be properly hashed and verifiable'
);
// Verify we can log in with the new user (simulates authentication)
$this->loginUser($user);
$this->client->request('GET', '/s/dashboard');
// Assert we can access the dashboard successfully
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('/s/dashboard', $this->client->getRequest()->getRequestUri());
}
}
/**
* @return array<string, array{userData: array<string, mixed>, expectedStatusCode: int}>
*/
public static function userCreateDataProvider(): array
{
return [
'valid user with password' => [
'userData' => [
'username' => 'john',
'plainPassword' => 'jjohn@123',
'firstName' => 'John',
'lastName' => 'Doe',
'email' => 'john.doe@email.com',
],
'expectedStatusCode' => Response::HTTP_CREATED,
],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Controller;
use Mautic\UserBundle\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\TestBrowserToken;
use Symfony\Component\HttpFoundation\Session\SessionFactory;
trait LoginUserWithSamlTrait
{
private function loginUserWithSaml(User $user): void
{
$firewallContext = 'mautic';
$token = new TestBrowserToken($user->getRoles(), $user, $firewallContext);
$container = $this->getContainer();
$container->get('security.untracked_token_storage')->setToken($token);
$session = self::getContainer()->get('session.factory')->createSession();
$session->set('samlsso', true);
$session->set('_security_'.$firewallContext, serialize($token));
$session->save();
$sessionFactory = $this->createMock(SessionFactory::class);
$sessionFactory->expects($this->any())
->method('createSession')
->willReturn($session);
self::getContainer()->set('session.factory', $sessionFactory);
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Tests\Traits\CreateEntityTrait;
use Symfony\Component\HttpFoundation\Request;
class ProfileControllerTest extends MauticMysqlTestCase
{
use CreateEntityTrait;
use LoginUserWithSamlTrait;
protected function setUp(): void
{
if (strpos($this->name(), 'WithSaml') > 0) {
$this->configParams['saml_idp_metadata'] = 'any_string';
}
parent::setUp();
}
public function testPasswordNotOnAccountPageWithSaml(): void
{
$user = $this->createUser($this->createRole(), 'test@example.com');
$this->em->flush();
$this->em->clear();
$this->loginUserWithSaml($user);
$this->client->request(Request::METHOD_GET, 's/account');
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
$this->assertStringNotContainsString('user[plainPassword][password]', $clientResponse->getContent());
$this->assertStringNotContainsString('user[plainPassword][confirm]', $clientResponse->getContent());
}
public function testPasswordOnAccountPageWithoutSaml(): void
{
$user = $this->createUser($this->createRole(), 'test@example.com');
$this->em->flush();
$this->em->clear();
$this->loginUser($user);
$this->client->request(Request::METHOD_GET, 's/account');
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
$this->assertStringContainsString('user[plainPassword][password]', $clientResponse->getContent());
$this->assertStringContainsString('user[plainPassword][confirm]', $clientResponse->getContent());
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
class SecurityControllerTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
if (strpos($this->name(), 'WithSaml') > 0) {
$this->configParams['saml_idp_metadata'] = 'any_string';
}
parent::setUp();
$this->logoutUser();
}
public function testLoginRetryPageShowsErrorWithSaml(): void
{
$this->client->request(Request::METHOD_GET, '/saml/login_retry');
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
$validationError = self::getContainer()->get('translator')->trans('mautic.user.security.saml.clearsession', [], 'flashes');
$this->assertStringContainsString($validationError, $clientResponse->getContent());
}
public function testLoginRetryPageRedirectsToLoginWithoutSaml(): void
{
$this->client->request(Request::METHOD_GET, '/saml/login_retry');
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
$validationError = self::getContainer()->get('translator')->trans('mautic.user.security.saml.clearsession', [], 'flashes');
$this->assertStringNotContainsString($validationError, $clientResponse->getContent());
$loginText = self::getContainer()->get('translator')->trans('mautic.user.auth.form.loginbtn', [], 'messages');
$this->assertStringContainsString($loginText, $clientResponse->getContent());
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Tests\Traits\CreateEntityTrait;
use Symfony\Component\HttpFoundation\Request;
class UserControllerTest extends MauticMysqlTestCase
{
use CreateEntityTrait;
use LoginUserWithSamlTrait;
protected function setUp(): void
{
if (strpos($this->name(), 'WithSaml') > 0) {
$this->configParams['saml_idp_metadata'] = 'any_string';
}
parent::setUp();
}
public function testPasswordFieldsOnEditUserPageWithSaml(): void
{
$user1 = $this->createUser($this->createRole(), 'test2@example.com');
$user2 = $this->createUser($this->createRole(true), 'test@example.com');
$this->em->flush();
$this->em->clear();
$this->loginUserWithSaml($user2);
$this->client->request(Request::METHOD_GET, 's/users/edit/'.$user1->getId());
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
$this->assertStringNotContainsString('user[plainPassword][password]', $clientResponse->getContent());
$this->assertStringNotContainsString('user[plainPassword][confirm]', $clientResponse->getContent());
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Mautic\UserBundle\Tests\Entity;
use Mautic\UserBundle\Entity\User;
class UserTest extends \PHPUnit\Framework\TestCase
{
public function testEraseCredentials(): void
{
$user = new User();
$user->setUsername('testUser');
$user->setPlainPassword('plainPass');
$user->setCurrentPassword('currentPass');
$user = unserialize(serialize($user));
\assert($user instanceof User);
$this->assertSame('testUser', $user->getUsername());
$this->assertNull($user->getPlainPassword());
$this->assertNull($user->getCurrentPassword());
}
public function testUserIsGuest(): void
{
$user = new User(true);
$this->assertTrue($user->isGuest());
}
public function testUserIsNotGuest(): void
{
$user = new User();
$this->assertFalse($user->isGuest());
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Mautic\UserBundle\Tests\Event;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Event\LoginEvent;
class LoginEventTest extends \PHPUnit\Framework\TestCase
{
public function testGetUser(): void
{
$user = $this->createMock(User::class);
$event = new LoginEvent($user);
$this->assertEquals($user, $event->getUser());
}
}

View File

@@ -0,0 +1,472 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\EventListener;
use FOS\OAuthServerBundle\Model\AccessToken;
use FOS\OAuthServerBundle\Security\Authenticator\Passport\Badge\AccessTokenBadge;
use FOS\OAuthServerBundle\Security\Authenticator\Token\OAuthToken;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\EventListener\ApiUserSubscriber;
use Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions;
use Mautic\UserBundle\Security\Provider\UserProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
class ApiUserSubscriberTest extends TestCase
{
public function testSubscribedEvents(): void
{
self::assertSame([
CheckPassportEvent::class => ['onCheckPassport', 2048],
AuthenticationTokenCreatedEvent::class => 'onTokenCreated',
], ApiUserSubscriber::getSubscribedEvents());
}
public function testIfAuthenticationHasNoUserInvolved(): void
{
$passport = $this->createMock(Passport::class);
$passport->expects(self::once())
->method('hasBadge')
->with(UserBadge::class)
->willReturn(false);
$passport->expects(self::never())
->method('getBadge');
$event = $this->createMock(CheckPassportEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::never())
->method('loadUserByIdentifier');
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onCheckPassport($event);
}
public function testIfAuthenticationAlreadySetUserLoader(): void
{
$userBadge = $this->createMock(UserBadge::class);
$userBadge->expects(self::once())
->method('getUserLoader')
->willReturn(function () {});
$userBadge->expects(self::never())
->method('setUserLoader');
$passport = $this->createMock(Passport::class);
$passport->expects(self::once())
->method('hasBadge')
->with(UserBadge::class)
->willReturn(true);
$passport->expects(self::once())
->method('getBadge')
->with(UserBadge::class)
->willReturn($userBadge);
$passport->expects(self::never())
->method('addBadge');
$event = $this->createMock(CheckPassportEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::never())
->method('loadUserByIdentifier');
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onCheckPassport($event);
}
public function testIfAuthenticationIsNotOauthAuthentication(): void
{
$userBadge = $this->createMock(UserBadge::class);
$userBadge->expects(self::once())
->method('getUserLoader')
->willReturn(null);
$userBadge->expects(self::never())
->method('setUserLoader');
$passport = $this->createMock(Passport::class);
$passport->expects(self::exactly(2))
->method('hasBadge')
->willReturnCallback(static function (string $className): bool {
if (UserBadge::class === $className) {
return true;
}
if (AccessTokenBadge::class === $className) {
return false;
}
self::fail('Unknown badge class '.$className);
});
$passport->expects(self::once())
->method('getBadge')
->with(UserBadge::class)
->willReturn($userBadge);
$passport->expects(self::never())
->method('addBadge');
$event = $this->createMock(CheckPassportEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::never())
->method('loadUserByIdentifier');
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onCheckPassport($event);
}
public function testIfOauthAuthenticationAndIdentifierIsNotFound(): void
{
$userIdentifier = 'The user';
$userBadge = $this->createMock(UserBadge::class);
$userBadge->expects(self::once())
->method('getUserLoader')
->willReturn(null);
$accessToken = $this->createMock(AccessToken::class);
$accessTokenBadge = $this->createMock(AccessTokenBadge::class);
$accessTokenBadge->method('getAccessToken')->willReturn($accessToken);
$passport = $this->createMock(Passport::class);
$passport->expects(self::exactly(2))
->method('hasBadge')
->willReturnCallback(static function (string $className): bool {
if (UserBadge::class === $className) {
return true;
}
if (AccessTokenBadge::class === $className) {
return true;
}
self::fail('Unknown badge class '.$className);
});
$passport->expects(self::exactly(2))
->method('getBadge')
->willReturnCallback(function (string $className) use ($accessTokenBadge, $userBadge): BadgeInterface {
if (UserBadge::class === $className) {
return $userBadge;
}
if (AccessTokenBadge::class === $className) {
return $accessTokenBadge;
}
self::fail('Unknown badge requested '.$className);
});
// Not changing any badges.
$passport->expects(self::never())
->method('addBadge');
$event = $this->createMock(CheckPassportEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($userIdentifier)
->willThrowException(new UserNotFoundException());
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::once())
->method('setActivePermissionsOnAuthToken')
->with($accessToken)
->willReturn(null);
$userBadge->expects(self::once())
->method('setUserLoader')
// After update to PHP 8.2 change return type to `null`.
->willReturnCallback(function (callable $userLoader) use ($userIdentifier): ?UserInterface {
$loaderResult = $userLoader($userIdentifier);
self::assertNull($loaderResult);
return $loaderResult;
});
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onCheckPassport($event);
}
public function testIfOauthAuthenticationAndIdentifierIsUserFromLoader(): void
{
$userIdentifier = 'The user';
$userRoles = ['role' => 'test'];
$userBadge = $this->createMock(UserBadge::class);
$userBadge->expects(self::once())
->method('getUserLoader')
->willReturn(null);
$accessToken = $this->createMock(AccessToken::class);
$accessTokenBadge = $this->createMock(AccessTokenBadge::class);
$accessTokenBadge->method('getAccessToken')->willReturn($accessToken);
$passport = $this->createMock(Passport::class);
$passport->expects(self::exactly(2))
->method('hasBadge')
->willReturnCallback(static function (string $className): bool {
if (UserBadge::class === $className) {
return true;
}
if (AccessTokenBadge::class === $className) {
return true;
}
self::fail('Unknown badge class '.$className);
});
$passport->expects(self::exactly(2))
->method('getBadge')
->willReturnCallback(function (string $className) use ($accessTokenBadge, $userBadge): BadgeInterface {
if (UserBadge::class === $className) {
return $userBadge;
}
if (AccessTokenBadge::class === $className) {
return $accessTokenBadge;
}
self::fail('Unknown badge requested '.$className);
});
$passport->expects(self::once())
->method('addBadge')
->with(new AccessTokenBadge($accessToken, $userRoles));
$event = $this->createMock(CheckPassportEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$user = $this->createMock(User::class);
$user->method('getRoles')->willReturn($userRoles);
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($userIdentifier)
->willReturn($user);
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$userBadge->expects(self::once())
->method('setUserLoader')
->willReturnCallback(function (callable $userLoader) use ($userIdentifier): User {
$loaderResult = $userLoader($userIdentifier);
self::assertNotNull($loaderResult);
return $loaderResult;
});
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onCheckPassport($event);
}
public function testIfOauthAuthenticationAndIdentifierIsFromTokenPermisions(): void
{
$userIdentifier = 'The user';
$userRoles = ['role' => 'test'];
$userBadge = $this->createMock(UserBadge::class);
$userBadge->expects(self::once())
->method('getUserLoader')
->willReturn(null);
$accessToken = $this->createMock(AccessToken::class);
$accessTokenBadge = $this->createMock(AccessTokenBadge::class);
$accessTokenBadge->method('getAccessToken')->willReturn($accessToken);
$passport = $this->createMock(Passport::class);
$passport->expects(self::exactly(2))
->method('hasBadge')
->willReturnCallback(static function (string $className): bool {
if (UserBadge::class === $className) {
return true;
}
if (AccessTokenBadge::class === $className) {
return true;
}
self::fail('Unknown badge class '.$className);
});
$passport->expects(self::exactly(2))
->method('getBadge')
->willReturnCallback(function (string $className) use ($accessTokenBadge, $userBadge): BadgeInterface {
if (UserBadge::class === $className) {
return $userBadge;
}
if (AccessTokenBadge::class === $className) {
return $accessTokenBadge;
}
self::fail('Unknown badge requested '.$className);
});
$passport->expects(self::once())
->method('addBadge')
->with(new AccessTokenBadge($accessToken, $userRoles));
$event = $this->createMock(CheckPassportEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$user = $this->createMock(User::class);
$user->method('getRoles')->willReturn($userRoles);
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($userIdentifier)
->willThrowException(new UserNotFoundException());
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::once())
->method('setActivePermissionsOnAuthToken')
->with($accessToken)
->willReturn($user);
$userBadge->expects(self::once())
->method('setUserLoader')
->willReturnCallback(function (callable $userLoader) use ($userIdentifier): User {
$loaderResult = $userLoader($userIdentifier);
self::assertNotNull($loaderResult);
return $loaderResult;
});
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onCheckPassport($event);
}
public function testTokenCreatedNotOauthToken(): void
{
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::never())
->method('loadUserByIdentifier');
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$passport = $this->createMock(Passport::class);
$passport->expects(self::once())
->method('hasBadge')
->with(AccessTokenBadge::class)
->willReturn(false);
$event = $this->createMock(AuthenticationTokenCreatedEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$event->expects(self::never())
->method('getAuthenticatedToken');
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onTokenCreated($event);
}
public function testTokenCreateOauthAlreadyHasAuthenticatedUser(): void
{
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::never())
->method('loadUserByIdentifier');
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$accessTokenBadge = $this->createMock(AccessTokenBadge::class);
$authenticatedToken = $this->createMock(OAuthToken::class);
$authenticatedToken->method('getUser')->willReturn($this->createMock(UserInterface::class));
// No user was replaced.
$authenticatedToken->expects(self::never())
->method('setUser');
$passport = $this->createMock(Passport::class);
$passport->expects(self::once())
->method('hasBadge')
->with(AccessTokenBadge::class)
->willReturn(true);
$passport->method('getBadge')->with(AccessTokenBadge::class)->willReturn($accessTokenBadge);
$event = $this->createMock(AuthenticationTokenCreatedEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$event->expects(self::once())
->method('getAuthenticatedToken')
->willReturn($authenticatedToken);
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onTokenCreated($event);
}
public function testTokenCreateOauthSetsAuthenticatedUser(): void
{
$userProvider = $this->createMock(UserProvider::class);
$userProvider->expects(self::never())
->method('loadUserByIdentifier');
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::never())
->method('setActivePermissionsOnAuthToken');
$user = $this->createMock(UserInterface::class);
$accessToken = $this->createMock(AccessToken::class);
$accessToken->method('getUser')->willReturn($user);
$accessTokenBadge = $this->createMock(AccessTokenBadge::class);
$accessTokenBadge->method('getAccessToken')->willReturn($accessToken);
$authenticatedToken = $this->createMock(OAuthToken::class);
$authenticatedToken->method('getUser')->willReturn(null);
// Replace the user from oAuth token.
$authenticatedToken->expects(self::once())
->method('setUser')
->with($user);
$passport = $this->createMock(Passport::class);
$passport->expects(self::once())
->method('hasBadge')
->with(AccessTokenBadge::class)
->willReturn(true);
$passport->method('getBadge')->with(AccessTokenBadge::class)->willReturn($accessTokenBadge);
$event = $this->createMock(AuthenticationTokenCreatedEvent::class);
$event->expects(self::once())
->method('getPassport')
->willReturn($passport);
$event->expects(self::once())
->method('getAuthenticatedToken')
->willReturn($authenticatedToken);
$subscriber = new ApiUserSubscriber($userProvider, $tokenPermissions);
$subscriber->onTokenCreated($event);
}
}

View File

@@ -0,0 +1,324 @@
<?php
namespace Mautic\UserBundle\Tests\EventListener;
use Mautic\ConfigBundle\Event\ConfigEvent;
use Mautic\UserBundle\EventListener\ConfigSubscriber;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ConfigSubscriberTest extends TestCase
{
/**
* @var ConfigEvent|MockObject
*/
private MockObject $configEvent;
protected function setUp(): void
{
$this->configEvent = $this->createMock(ConfigEvent::class);
}
public function testOwnPasswordIsNotWipedOutOnConfigSaveIfEmpty(): void
{
$subscriber = new ConfigSubscriber();
$this->configEvent->expects($this->once())
->method('unsetIfEmpty')
->with('saml_idp_own_password');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn([]);
$subscriber->onConfigSave($this->configEvent);
}
public function testMetadataFileIsDetectedAsXml(): void
{
$subscriber = new ConfigSubscriber();
$this->configEvent->expects($this->once())
->method('unsetIfEmpty')
->with('saml_idp_own_password');
$file = $this->createMock(UploadedFile::class);
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn('<xml></xml>');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_metadata' => $file,
]
);
$this->configEvent->expects($this->never())
->method('setError');
$subscriber->onConfigSave($this->configEvent);
}
public function testMetadataFileFailsValidationIfNotXml(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn('foobar');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_metadata' => $file,
]
);
$this->configEvent->expects($this->once())
->method('setError')
->with('mautic.user.saml.metadata.invalid', [], 'userconfig', 'saml_idp_metadata');
$subscriber->onConfigSave($this->configEvent);
}
public function testCertificatePassesValidationIfValid(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn('-----BEGIN CERTIFICATE-----');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_certificate' => $file,
]
);
$this->configEvent->expects($this->never())
->method('setError');
$subscriber->onConfigSave($this->configEvent);
}
public function testCertificateFailsValidationIfNotValid(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn('foobar');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_certificate' => $file,
]
);
$this->configEvent->expects($this->once())
->method('setError')
->with('mautic.user.saml.certificate.invalid', [], 'userconfig', 'saml_idp_own_certificate');
$subscriber->onConfigSave($this->configEvent);
}
public function testPrivateKeyPassesValidationIfValid(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn('-----BEGIN RSA PRIVATE KEY-----');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_private_key' => $file,
]
);
$this->configEvent->expects($this->never())
->method('setError');
$subscriber->onConfigSave($this->configEvent);
}
public function testPrivateKeyFailsValidationIfNotValid(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn('foobar');
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_private_key' => $file,
]
);
$this->configEvent->expects($this->once())
->method('setError')
->with('mautic.user.saml.private_key.invalid', [], 'userconfig', 'saml_idp_own_private_key');
$subscriber->onConfigSave($this->configEvent);
}
public function testEncryptedPrivateKeyPassesValidationIfValid(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$key = <<<KEY_WRAP
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI+tgT3QFhjEgCAggA
MBQGCCqGSIb3DQMHBAg1HGrSHb7zVwSCAoBG89zqAxAx+vPvhQVW6dfJFBTSpAGq
RRlfiygr0iwLCxpnoT5ZY/0Od28uvB/HkAb0cCg6sYvNVvDDDitspCb3vIU/gPmN
h8VEtgxDlXEE1YwinDn0BfO5tRoC4SfOrJRsrRdX4qc7xk80Kk74cbzLJy6VESQ5
u/ZY8Yw2E9exIH4rlZ+dBfs4JekQS/fLbiXGdtSdVF6RO0e9IhRsb3dUhSqqJafl
6+VkptheXGFwTySf8c5yLmMUC/z2NCGxO5G91uUxshEQvdQ2NM0kvt9AG3TcwZ6g
obqjHWfVEmT9j9Raqbkn+9desalKRONbe1lI0IL1vcIBXWduQUNQK1dT9ghbwGT+
vegw86xlMtLNPrvFxc3G6ZVtid/T1wDXZEKKCqp9Uei3fh0SLKy2witjt5yvabbc
QhXOS2hVA9zSQ0IGcycWzxeaf+Nb826xvvAV9Tf1GhreT6vQWC8BxDCVr9w31h6y
LnC5R2dxzom1d/kiBqlcGh1u9d+2OyCFpfYvymWlKcXYYO8E2Nu2oK4IlzB0YdJp
8/y7PLGzaipf57e8srGMGvLMKwQTLCvaDu/gxl4d+awwTZ9UsG2qMOKZSkIU4IMu
uP0CDSbRxbHBq9gY4ZiECuAxmmoadZXmNOu8iGQWa9rmoCqW5XmAgwvcnfxDWawI
ZTJUgHdZ6Tfe2jPFJjSZVoU/en0W5BQXgy1u3BDX68C8nAfZ4xmeyELcMub9hTYb
HTDmYIBozZNIcYHB6OGZnURuGeofMVJqMkNfnEuSuoCJsXGIhLznDKp8G00F9eR2
1L+B0ZUZv/O82qEGzC/IX7+CFmSDStV9R400cDvi+8BsdMMB+WV6SMnK
-----END ENCRYPTED PRIVATE KEY-----
KEY_WRAP;
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn($key);
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_private_key' => $file,
'saml_idp_own_password' => 'abc123',
]
);
$this->configEvent->expects($this->never())
->method('setError');
$subscriber->onConfigSave($this->configEvent);
}
public function testPrivateKeyFailsValidationIfPasswordNotValid(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$key = <<<KEY_WRAP
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI+tgT3QFhjEgCAggA
MBQGCCqGSIb3DQMHBAg1HGrSHb7zVwSCAoBG89zqAxAx+vPvhQVW6dfJFBTSpAGq
RRlfiygr0iwLCxpnoT5ZY/0Od28uvB/HkAb0cCg6sYvNVvDDDitspCb3vIU/gPmN
h8VEtgxDlXEE1YwinDn0BfO5tRoC4SfOrJRsrRdX4qc7xk80Kk74cbzLJy6VESQ5
u/ZY8Yw2E9exIH4rlZ+dBfs4JekQS/fLbiXGdtSdVF6RO0e9IhRsb3dUhSqqJafl
6+VkptheXGFwTySf8c5yLmMUC/z2NCGxO5G91uUxshEQvdQ2NM0kvt9AG3TcwZ6g
obqjHWfVEmT9j9Raqbkn+9desalKRONbe1lI0IL1vcIBXWduQUNQK1dT9ghbwGT+
vegw86xlMtLNPrvFxc3G6ZVtid/T1wDXZEKKCqp9Uei3fh0SLKy2witjt5yvabbc
QhXOS2hVA9zSQ0IGcycWzxeaf+Nb826xvvAV9Tf1GhreT6vQWC8BxDCVr9w31h6y
LnC5R2dxzom1d/kiBqlcGh1u9d+2OyCFpfYvymWlKcXYYO8E2Nu2oK4IlzB0YdJp
8/y7PLGzaipf57e8srGMGvLMKwQTLCvaDu/gxl4d+awwTZ9UsG2qMOKZSkIU4IMu
uP0CDSbRxbHBq9gY4ZiECuAxmmoadZXmNOu8iGQWa9rmoCqW5XmAgwvcnfxDWawI
ZTJUgHdZ6Tfe2jPFJjSZVoU/en0W5BQXgy1u3BDX68C8nAfZ4xmeyELcMub9hTYb
HTDmYIBozZNIcYHB6OGZnURuGeofMVJqMkNfnEuSuoCJsXGIhLznDKp8G00F9eR2
1L+B0ZUZv/O82qEGzC/IX7+CFmSDStV9R400cDvi+8BsdMMB+WV6SMnK
-----END ENCRYPTED PRIVATE KEY-----
KEY_WRAP;
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn($key);
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_private_key' => $file,
'saml_idp_own_password' => '123abc',
]
);
$this->configEvent->expects($this->once())
->method('setError')
->with('mautic.user.saml.private_key.password_invalid', [], 'userconfig', 'saml_idp_own_password');
$subscriber->onConfigSave($this->configEvent);
}
public function testPrivateKeyFailsValidationIfPasswordMissing(): void
{
$subscriber = new ConfigSubscriber();
$file = $this->createMock(UploadedFile::class);
$key = <<<KEY_WRAP
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI+tgT3QFhjEgCAggA
MBQGCCqGSIb3DQMHBAg1HGrSHb7zVwSCAoBG89zqAxAx+vPvhQVW6dfJFBTSpAGq
RRlfiygr0iwLCxpnoT5ZY/0Od28uvB/HkAb0cCg6sYvNVvDDDitspCb3vIU/gPmN
h8VEtgxDlXEE1YwinDn0BfO5tRoC4SfOrJRsrRdX4qc7xk80Kk74cbzLJy6VESQ5
u/ZY8Yw2E9exIH4rlZ+dBfs4JekQS/fLbiXGdtSdVF6RO0e9IhRsb3dUhSqqJafl
6+VkptheXGFwTySf8c5yLmMUC/z2NCGxO5G91uUxshEQvdQ2NM0kvt9AG3TcwZ6g
obqjHWfVEmT9j9Raqbkn+9desalKRONbe1lI0IL1vcIBXWduQUNQK1dT9ghbwGT+
vegw86xlMtLNPrvFxc3G6ZVtid/T1wDXZEKKCqp9Uei3fh0SLKy2witjt5yvabbc
QhXOS2hVA9zSQ0IGcycWzxeaf+Nb826xvvAV9Tf1GhreT6vQWC8BxDCVr9w31h6y
LnC5R2dxzom1d/kiBqlcGh1u9d+2OyCFpfYvymWlKcXYYO8E2Nu2oK4IlzB0YdJp
8/y7PLGzaipf57e8srGMGvLMKwQTLCvaDu/gxl4d+awwTZ9UsG2qMOKZSkIU4IMu
uP0CDSbRxbHBq9gY4ZiECuAxmmoadZXmNOu8iGQWa9rmoCqW5XmAgwvcnfxDWawI
ZTJUgHdZ6Tfe2jPFJjSZVoU/en0W5BQXgy1u3BDX68C8nAfZ4xmeyELcMub9hTYb
HTDmYIBozZNIcYHB6OGZnURuGeofMVJqMkNfnEuSuoCJsXGIhLznDKp8G00F9eR2
1L+B0ZUZv/O82qEGzC/IX7+CFmSDStV9R400cDvi+8BsdMMB+WV6SMnK
-----END ENCRYPTED PRIVATE KEY-----
KEY_WRAP;
$this->configEvent->expects($this->once())
->method('getFileContent')
->willReturn($key);
$this->configEvent->expects($this->once())
->method('getConfig')
->with('userconfig')
->willReturn(
[
'saml_idp_own_private_key' => $file,
'saml_idp_own_password' => '',
]
);
$this->configEvent->expects($this->once())
->method('setError')
->with('mautic.user.saml.private_key.password_needed', [], 'userconfig', 'saml_idp_own_password');
$subscriber->onConfigSave($this->configEvent);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\EventListener;
use LightSaml\Context\Profile\MessageContext;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Protocol\Response as LightSamlResponse;
use LightSaml\Model\Protocol\Status;
use LightSaml\Model\Protocol\StatusCode;
use Mautic\CoreBundle\EventListener\ExceptionListener;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\Router;
class LightSAMLExceptionListenerTest extends MauticMysqlTestCase
{
/**
* @var MockObject|LoggerInterface
*/
private $logger;
protected function setUp(): void
{
parent::setup();
$this->logger = $this->createMock(LoggerInterface::class);
$this->router = $this->createMock(Router::class);
$this->router->expects($this->once())->method('generate')->willReturn('saml/login_retry');
}
public function testSamlRoutesAreRedirectedToDefaultLoginIfSamlIsDisabled(): void
{
// creating a success status
$statusCode = new StatusCode('urn:oasis:names:tc:SAML:2.0:status:Success');
$status = new Status($statusCode);
// creating a saml response which will return above status
$lightSAMLResponse = $this->createMock(LightSamlResponse::class);
$lightSAMLResponse->expects($this->any())->method('getStatus')->willReturn($status);
// creating inbound context which will return lightsaml response
$inboundContext = $this->createMock(MessageContext::class);
$inboundContext->expects($this->exactly(2))->method('getMessage')->willReturn($lightSAMLResponse);
// creating context which will return inbound context
$context = $this->createMock(ProfileContext::class);
$context->expects($this->exactly(2))->method('getInboundContext')->willReturn($inboundContext);
// creating exception which will requires context
$exception = new LightSamlContextException($context, 'Unknown Inresponse');
$request = new Request();
$session = $this->createMock(Session::class);
$request->attributes->set('_session', $session);
$kernel = $this->createMock(HttpKernelInterface::class);
$event = new ExceptionEvent(
$kernel,
$request,
HttpKernelInterface::MAIN_REQUEST,
$exception
);
$subscriber = new ExceptionListener($this->router, 'MauticCoreBundle:Exception:show', $this->logger);
$subscriber->onKernelException($event);
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\EventListener;
use Mautic\UserBundle\EventListener\PasswordStrengthSubscriber;
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PasswordStrengthBadge;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
class PasswordStrengthSubscriberTest extends TestCase
{
public function testNoCheckPassportEvent(): void
{
$passport = $this->createMock(Passport::class);
$passport->method('hasBadge')
->with(PasswordCredentials::class)
->willReturn(false);
$passport->expects(self::never())
->method('getBadge');
$event = $this->createMock(CheckPassportEvent::class);
$event->method('getPassport')
->willReturn($passport);
$subscriber = new PasswordStrengthSubscriber();
$subscriber->checkPassport($event);
}
public function testCheckPassportEvent(): void
{
$password = 'Keilschrift';
$passwordCredentialsBadge = $this->createMock(PasswordCredentials::class);
$passwordCredentialsBadge->method('getPassword')
->willReturn($password);
$passport = $this->createMock(Passport::class);
$passport->method('hasBadge')
->with(PasswordCredentials::class)
->willReturn(true);
$passport->expects(self::once())
->method('getBadge')
->with(PasswordCredentials::class)
->willReturn($passwordCredentialsBadge);
$passport->expects(self::once())
->method('addBadge')
->willReturnCallback(static function (PasswordStrengthBadge $badge) use ($passport, $password): Passport {
self::assertSame($password, $badge->getPresentedPassword());
return $passport;
});
$event = $this->createMock(CheckPassportEvent::class);
$event->method('getPassport')
->willReturn($passport);
$subscriber = new PasswordStrengthSubscriber();
$subscriber->checkPassport($event);
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\EventListener;
use Mautic\UserBundle\Event\AuthenticationEvent;
use Mautic\UserBundle\EventListener\PasswordSubscriber;
use Mautic\UserBundle\Exception\WeakPasswordException;
use Mautic\UserBundle\Model\PasswordStrengthEstimatorModel;
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PasswordStrengthBadge;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
final class PasswordSubscriberTest extends TestCase
{
private PasswordSubscriber $passwordSubscriber;
private PasswordStrengthEstimatorModel $passwordStrengthEstimatorModel;
/**
* @var MockObject&AuthenticationEvent
*/
private $authenticationEvent;
/**
* @var MockObject&PluginToken
*/
private $pluginToken;
/**
* @var MockObject&EventDispatcherInterface
*/
private $dispatcher;
protected function setUp(): void
{
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->passwordStrengthEstimatorModel = new PasswordStrengthEstimatorModel($this->dispatcher);
$this->passwordSubscriber = new PasswordSubscriber($this->passwordStrengthEstimatorModel);
$this->authenticationEvent = $this->createMock(AuthenticationEvent::class);
$this->pluginToken = $this->createMock(PluginToken::class);
$this->authenticationEvent->expects($this->any())
->method('getToken')
->willReturn($this->pluginToken);
}
public function testThatItIsSubscribedToEvents(): void
{
$subscribedEvents = PasswordSubscriber::getSubscribedEvents();
Assert::assertCount(1, $subscribedEvents);
Assert::assertArrayHasKey(CheckPassportEvent::class, $subscribedEvents);
}
public function testThatItThrowsExceptionIfPasswordIsWeak(): void
{
$this->expectException(WeakPasswordException::class);
$passwordStrengthBadge = new PasswordStrengthBadge('11111111');
$this->passwordSubscriber->checkPassport(
new CheckPassportEvent(
$this->createMock(AuthenticatorInterface::class),
new Passport(
$this->createMock(UserBadge::class),
$this->createMock(CredentialsInterface::class),
[$passwordStrengthBadge]
)
)
);
}
public function testThatItDoesntThrowExceptionIfPasswordIsStrong(): void
{
$passwordStrengthBadge = new PasswordStrengthBadge(uniqid('password_strength', true));
$this->passwordSubscriber->checkPassport(
new CheckPassportEvent(
$this->createMock(AuthenticatorInterface::class),
new Passport(
$this->createMock(UserBadge::class),
$this->createMock(CredentialsInterface::class),
[$passwordStrengthBadge]
)
)
);
$this->addToAssertionCount(1); // Verify that no exception is thrown
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Mautic\UserBundle\Tests\EventListener;
use Mautic\UserBundle\EventListener\SAMLSubscriber;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\Router;
class SAMLSubscriberTest extends TestCase
{
/**
* @var RequestEvent&MockObject
*/
private MockObject $event;
/**
* @var Router&MockObject
*/
private MockObject $router;
protected function setUp(): void
{
$this->event = $this->createMock(RequestEvent::class);
$this->event->expects($this->once())
->method('isMainRequest')
->willReturn(true);
$this->router = $this->createMock(Router::class);
}
/**
* Because this subscriber is removed from the kernel if the SAML is disabled,
* this need to be tested always in the case it's enabled.
*/
public function testRedirectIsIgnoredIfSamlEnabled(): void
{
$redirect = '/redirect';
$subscriber = new SAMLSubscriber($this->router);
$request = $this->createMock(Request::class);
$request->attributes = new ParameterBag();
$request->method('getRequestUri')
->willReturn('/saml/login');
$this->event->method('getRequest')
->willReturn($request);
$this->router->expects($this->once())
->method('generate')
->with('login')
->willReturn($redirect);
$this->event->expects($this->once())
->method('setResponse')
->with(new RedirectResponse($redirect));
$subscriber->onKernelRequest($this->event);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Mautic\UserBundle\Tests\EventListener;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Event\LoginEvent;
use Mautic\UserBundle\EventListener\SecuritySubscriber;
use Mautic\UserBundle\UserEvents;
class SecuritySubscriberTest extends \PHPUnit\Framework\TestCase
{
public function testGetSubscribedEvents(): void
{
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
$auditLogModel = $this->createMock(AuditLogModel::class);
$subscriber = new SecuritySubscriber($ipLookupHelper, $auditLogModel);
$this->assertEquals(
[
UserEvents::USER_LOGIN => ['onSecurityInteractiveLogin', 0],
],
$subscriber->getSubscribedEvents()
);
}
public function testOnSecurityInteractiveLogin(): void
{
$userId = 132564;
$userName = 'John Doe';
$ip = '125.55.45.21';
$log = [
'bundle' => 'user',
'object' => 'security',
'objectId' => $userId,
'action' => 'login',
'details' => ['username' => $userName],
'ipAddress' => $ip,
];
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
$ipLookupHelper->expects($this->once())
->method('getIpAddressFromRequest')
->willReturn($ip);
$auditLogModel = $this->createMock(AuditLogModel::class);
$auditLogModel->expects($this->once())
->method('writeToLog')
->with($log);
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn($userId);
$user->expects($this->once())
->method('getUserIdentifier')
->willReturn($userName);
$event = $this->createMock(LoginEvent::class);
$event->expects($this->exactly(2))
->method('getUser')
->willReturn($user);
$subscriber = new SecuritySubscriber($ipLookupHelper, $auditLogModel);
$subscriber->onSecurityInteractiveLogin($event);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Functional\ApiPlatform;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\User;
/**
* Tests that the User API endpoints properly handle password as write-only field.
*
* The password field should:
* - Accept values when creating/updating users (write-only via user:write group)
* - NEVER be returned in API responses (not in user:read group)
* - Be hashed before storage in the database
*
* This ensures that password hashes are never exposed through the API,
* which is critical for security.
*/
final class UserApiTest extends MauticMysqlTestCase
{
protected function beforeBeginTransaction(): void
{
$this->resetAutoincrement([
'users',
'roles',
]);
}
/**
* Test that password hash is not exposed in API GET responses.
*/
public function testPasswordHashNotExposedInGet(): void
{
// Use the default admin user that exists in the database
$adminUser = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
$this->assertNotNull($adminUser, 'Admin user should exist');
$userId = $adminUser->getId();
// Test GET - password should not be in response
$this->client->request('GET', "/api/v2/users/{$userId}");
$this->assertResponseIsSuccessful();
$responseData = json_decode($this->client->getResponse()->getContent(), true);
// Assert password is not in the response
$this->assertArrayNotHasKey('password', $responseData);
$this->assertArrayHasKey('id', $responseData);
$this->assertArrayHasKey('username', $responseData);
$this->assertSame('admin', $responseData['username']);
}
/**
* Test that password is not exposed in GET collection endpoint.
*/
public function testPasswordNotExposedInCollection(): void
{
// Test GET collection
$this->client->request('GET', '/api/v2/users');
$this->assertResponseIsSuccessful();
$responseData = json_decode($this->client->getResponse()->getContent(), true);
// ApiPlatform uses 'member' for collection items
$this->assertArrayHasKey('member', $responseData);
$this->assertIsArray($responseData['member']);
$this->assertNotEmpty($responseData['member'], 'Should have at least one user');
// Check each user in the collection
foreach ($responseData['member'] as $userData) {
$this->assertArrayNotHasKey('password', $userData, 'Password should not be exposed in collection');
$this->assertArrayHasKey('username', $userData);
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
class PublicControllerTest extends MauticMysqlTestCase
{
private const PASSWORD_RESET_URI = '/passwordreset';
protected function setUp(): void
{
if (strpos($this->name(), 'WithSaml') > 0) {
$this->configParams['saml_idp_metadata'] = 'any_string';
}
parent::setUp();
}
/**
* Tests to ensure that xss is prevented on password reset page.
*/
public function testXssFilterOnPasswordReset(): void
{
$this->client->request(Request::METHOD_GET, self::PASSWORD_RESET_URI.'?bundle=%27-alert("XSS%20TEST%20Mautic")-%27');
$clientResponse = $this->client->getResponse();
$this->assertSame(200, $clientResponse->getStatusCode(), 'Return code must be 200.');
$responseData = $clientResponse->getContent();
// Tests that actual string is not present.
$this->assertStringNotContainsString('-alert("xss test mautic")-', $responseData, 'XSS injection attempt is filtered.');
// Tests that sanitized string is passed.
$this->assertStringContainsString('alertxsstestmautic', $responseData, 'XSS sanitized string is present.');
}
public function testPasswordResetPage(): void
{
$this->client->request(Request::METHOD_GET, self::PASSWORD_RESET_URI);
$clientResponse = $this->client->getResponse();
$this->assertSame(200, $clientResponse->getStatusCode(), 'Return code must be 200.');
$responseData = $clientResponse->getContent();
$this->assertStringContainsString('Enter either your username or email to reset your password. Instructions to reset your password will be sent to the email in your profile.', $responseData);
}
public function testPasswordResetAction(): void
{
$crawler = $this->client->request(Request::METHOD_GET, self::PASSWORD_RESET_URI);
$saveButton = $crawler->selectButton('Reset password');
$form = $saveButton->form();
$form['passwordreset[identifier]']->setValue('test@example.com');
$this->client->submit($form);
$clientResponse = $this->client->getResponse();
$this->assertTrue($clientResponse->isOk(), $clientResponse->getContent());
$responseData = $clientResponse->getContent();
$this->assertStringContainsString('A new password has been generated and will be emailed to you, if this user exists. If you do not receive it within a few minutes, check your spam box and/or contact the system administrator.', $responseData);
}
public function testPasswordResetActionWithoutUserWithSaml(): void
{
$crawler = $this->client->request(Request::METHOD_GET, self::PASSWORD_RESET_URI);
// Get the form
$form = $crawler->filter('form')->form();
$form->setValues([
'passwordreset[identifier]' => 'test2@example.com',
]);
$this->client->submit($form);
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
$this->assertStringContainsString('A new password has been generated and will be emailed to you, if this user exists. If you do not receive it within a few minutes, check your spam box and/or contact the system administrator.', $clientResponse->getContent());
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\Role;
use Symfony\Component\HttpFoundation\Request;
class RoleControllerFunctionalTest extends MauticMysqlTestCase
{
public function testNewRoleAction(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/roles/new');
$saveButton = $crawler->selectButton('role[buttons][apply]');
$name = 'Test Role';
$desc = 'Role Description';
$form = $saveButton->form();
$form['role[name]']->setValue($name);
$form['role[description]']->setValue($desc);
$this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
$this->assertStringContainsString($name, $this->client->getResponse()->getContent());
$this->assertStringContainsString($desc, $this->client->getResponse()->getContent());
}
public function testEditRoleAction(): void
{
$role = new Role();
$role->setName('Test Role');
$role->setDescription('The Description');
$this->em->persist($role);
$this->em->flush();
$crawler = $this->client->request(Request::METHOD_GET, '/s/roles/edit/'.$role->getId());
$saveButton = $crawler->selectButton('role[buttons][save]');
$updatedName = 'Test Role Updated';
$form = $saveButton->form();
$form['role[name]']->setValue($updatedName);
$this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk());
$this->assertStringContainsString($updatedName, $this->client->getResponse()->getContent());
}
}

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Entity\AuditLog;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use Symfony\Component\HttpFoundation\Response;
class UserControllerFunctionalTest extends MauticMysqlTestCase
{
protected function setUp(): void
{
$this->configParams += [
'saml_idp_own_private_key' => 'any_string',
];
parent::setUp();
}
public function testEditGetPage(): void
{
$this->client->request('GET', '/s/users/edit/1');
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
}
public function testRedirectNonExistingUser(): void
{
$crawler = $this->client->request('GET', '/s/users/edit/00000');
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$this->assertStringContainsString('Users', $crawler->filter('h1')->text());
$this->assertStringContainsString('User not found with', $crawler->filter('#flashes')->text());
}
public function testEditActionFormSubmissionValid(): void
{
$crawler = $this->client->request('GET', '/s/users/edit/1');
$buttonCrawlerNode = $crawler->selectButton('Save & Close');
$form = $buttonCrawlerNode->form();
$form['user[firstName]'] = 'test';
$this->client->submit($form);
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
$this->assertStringContainsString('has been updated!', $response->getContent());
}
public function testEditActionFormSubmissionInvalid(): void
{
$crawler = $this->client->request('GET', '/s/users/edit/1');
$form = $crawler->selectButton('Save')->form([
'user[firstName]' => '',
'user[lastName]' => '',
'user[email]' => 'invalid-email',
'user[plainPassword][password]' => '',
]);
$this->client->submit($form);
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$this->assertStringContainsString('The email entered is invalid.', $this->client->getResponse()->getContent());
}
/**
* @param array<string, string> $data
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataNewUserForPasswordField')]
public function testNewUserForPasswordField(array $data, string $message): void
{
$crawler = $this->client->request('GET', '/s/users/new');
$formData = [
'user[firstName]' => 'John',
'user[lastName]' => 'Doe',
'user[email]' => 'john.doe@example.com',
];
$form = $crawler->selectButton('Save')->form($formData + $data);
$this->client->submit($form);
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$this->assertStringContainsString($message, $this->client->getResponse()->getContent());
}
/**
* @return iterable<string, array<int, string|array<string, string>>>
*/
public static function dataNewUserForPasswordField(): iterable
{
yield 'Blank' => [
[
'user[plainPassword][password]' => '',
'user[plainPassword][confirm]' => '',
],
'Password cannot be blank.',
];
yield 'Do not match with confirm' => [
[
'user[plainPassword][password]' => 'same',
],
'Passwords do not match.',
];
yield 'Minimum length' => [
[
'user[plainPassword][password]' => 'same',
'user[plainPassword][confirm]' => 'same',
],
'Password must be at least 6 characters.',
];
yield 'No stronger' => [
[
'user[plainPassword][password]' => 'same123',
'user[plainPassword][confirm]' => 'same123',
],
'Please enter a stronger password. Your password must use a combination of upper and lower case, special characters and numbers.',
];
}
/**
* @param array<string, string> $data
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataForEditUserForPasswordField')]
public function testEditUserForPasswordField(array $data, string $message): void
{
$crawler = $this->client->request('GET', '/s/users/edit/1');
$form = $crawler->selectButton('Save')->form($data);
$this->client->submit($form);
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
$this->assertStringContainsString($message, $this->client->getResponse()->getContent());
}
/**
* @return iterable<string, array<int, string|array<string, string>>>
*/
public static function dataForEditUserForPasswordField(): iterable
{
yield 'Do not match with confirm' => [
[
'user[plainPassword][password]' => 'same',
],
'Passwords do not match.',
];
yield 'Minimum length' => [
[
'user[plainPassword][password]' => 'same',
'user[plainPassword][confirm]' => 'same',
],
'Password must be at least 6 characters.',
];
yield 'No stronger' => [
[
'user[plainPassword][password]' => 'same123',
'user[plainPassword][confirm]' => 'same123',
],
'Please enter a stronger password. Your password must use a combination of upper and lower case, special characters and numbers.',
];
}
/**
* @param array<mixed> $details
*/
public function auditLogSetter(
int $userId,
string $userName,
string $bundle,
string $object,
int $objectId,
string $action,
array $details,
): AuditLog {
$auditLog = new AuditLog();
$auditLog->setUserId($userId);
$auditLog->setUserName($userName);
$auditLog->setBundle($bundle);
$auditLog->setObject($object);
$auditLog->setObjectId($objectId);
$auditLog->setAction($action);
$auditLog->setDetails($details);
$auditLog->setDateAdded(new \DateTime());
$auditLog->setIpAddress('127.0.0.1');
return $auditLog;
}
public function userSetter(Role $role): User
{
$user = new User();
$user->setUsername('testuser');
$user->setEmail('test@email.com');
$user->setFirstName('Test');
$user->setLastName('User');
$user->setPassword('password');
$user->setRole($role);
$user->setLastLogin('2024-02-22 10:30:00');
return $user;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Response;
final class SearchTest extends MauticMysqlTestCase
{
public function testSearchingUsersByName(): void
{
$this->client->request('GET', 's/users?search=name:admin');
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
$this->assertStringContainsString('admin', $this->client->getResponse()->getContent());
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Functional;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class UserLogoutFunctionalTest extends MauticMysqlTestCase
{
public function testLogout(): void
{
$role = new Role();
$role->setName('Role');
$role->setIsAdmin(true);
$this->em->persist($role);
$user = new User();
$user->setFirstName('John');
$user->setLastName('Doe');
$user->setUsername('john.doe');
$user->setEmail('john.doe@email.com');
$user->setRole($role);
$hasher = static::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
\assert($hasher instanceof PasswordHasherInterface);
$user->setPassword($hasher->hash('Maut1cR0cks!'));
$this->em->persist($user);
$this->em->flush();
$this->em->clear();
// Login newly created non-admin user
$this->loginUser($user);
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
$this->client->request(Request::METHOD_GET, '/s/logout');
$clientResponse = $this->client->getResponse();
Assert::assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
Assert::assertStringContainsString(
'login',
$clientResponse->getContent()
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Model;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\RoleRepository;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Form\Validator\Constraints\NotWeak;
use PHPUnit\Framework\Assert;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class PasswordStrengthEstimatorModelTest extends MauticMysqlTestCase
{
private PasswordHasherFactoryInterface $passwordHasher;
private RoleRepository $roleRepository;
private ValidatorInterface $validator;
protected function setUp(): void
{
parent::setUp();
$this->passwordHasher = self::getContainer()->get('security.password_hasher_factory');
$this->roleRepository = $this->em->getRepository(Role::class);
$this->validator = static::getContainer()->get('validator');
}
public function testThatItIsNotPossibleToCreateAnUserWithAWeakPassword(): void
{
$simplePassword = '11111111';
$user = new User();
$user->setFirstName('First Name');
$user->setLastName('LastName');
$user->setUsername('username');
$user->setEmail('some@email.domain');
$user->setPlainPassword($simplePassword);
$user->setPassword($this->passwordHasher->getPasswordHasher($user)->hash($simplePassword));
$user->setRole($this->roleRepository->findAll()[0]);
$violations = $this->validator->validate($user);
$hasNotWeakConstraintViolation = false;
/** @var ConstraintViolation $violation */
foreach ($violations as $violation) {
$hasNotWeakConstraintViolation |= $violation->getConstraint() instanceof NotWeak;
}
Assert::assertGreaterThanOrEqual(1, count($violations));
Assert::assertTrue((bool) $hasNotWeakConstraintViolation);
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Mautic\UserBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Entity\UserToken;
use Mautic\UserBundle\Model\UserModel;
use Mautic\UserBundle\Model\UserToken\UserTokenServiceInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserModelTest extends TestCase
{
private UserModel $userModel;
/**
* @var MockObject&MailHelper
*/
private MockObject $mailHelper;
/**
* @var MockObject&EntityManager
*/
private MockObject $entityManager;
/**
* @var MockObject&Router
*/
private MockObject $router;
/**
* @var MockObject&TranslatorInterface
*/
private MockObject $translator;
/**
* @var MockObject&User
*/
private MockObject $user;
/**
* @var MockObject&UserToken
*/
private MockObject $userToken;
/**
* @var MockObject&UserTokenServiceInterface
*/
private MockObject $userTokenService;
/**
* @var MockObject&LoggerInterface
*/
private MockObject $logger;
public function setUp(): void
{
$this->mailHelper = $this->createMock(MailHelper::class);
$this->userTokenService = $this->createMock(UserTokenServiceInterface::class);
$this->entityManager = $this->createMock(EntityManager::class);
$this->user = $this->createMock(User::class);
$this->router = $this->createMock(Router::class);
$this->translator = $this->createMock(Translator::class);
$this->userToken = $this->createMock(UserToken::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->userModel = new UserModel(
$this->mailHelper,
$this->userTokenService,
$this->entityManager,
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->router,
$this->translator,
$this->createMock(UserHelper::class),
$this->logger,
$this->createMock(CoreParametersHelper::class)
);
}
public function testThatItSendsResetPasswordEmailAndRouterGetsCalledWithCorrectParamters(): void
{
$this->userTokenService->expects($this->once())
->method('generateSecret')
->willReturn($this->userToken);
$this->mailHelper
->method('getMailer')
->willReturn($this->mailHelper);
$this->mailHelper->expects($this->once())
->method('send');
$this->userTokenService->expects($this->once())
->method('generateSecret')
->willReturn($this->userToken);
$this->router->expects($this->once())
->method('generate')
->with('mautic_user_passwordresetconfirm', ['token' => null], UrlGeneratorInterface::ABSOLUTE_URL);
$this->translator
->expects($this->any())
->method('trans')
->willReturn('test');
$this->userModel->sendResetEmail($this->user);
}
public function testThatDatabaseErrorThrowsRuntimeExceptionAndItIsLoggedWhenWeTryToSaveTokenToTheDatabaseWhenWeSendResetPasswordEmail(): void
{
$errorMessage = 'Some error message';
$this->expectException(\RuntimeException::class);
$this->entityManager->expects($this->once())
->method('flush')
->willThrowException(new \Exception($errorMessage));
$this->logger->expects($this->once())
->method('error')
->with($errorMessage);
$this->userModel->sendResetEmail($this->user);
}
public function testEmailUser(): void
{
$email = 'a@test.com';
$name = 'name';
$toMail = [$email => $name];
$subject = 'subject';
$content = 'content';
$this->user->expects($this->once())
->method('getEmail')
->willReturn($email);
$this->user->expects($this->once())
->method('getName')
->willReturn($name);
$this->mailHelper->expects($this->once())
->method('getMailer')
->willReturn($this->mailHelper);
$this->mailHelper->expects($this->once())
->method('setTo')
->with($toMail)
->willReturn(true);
$this->mailHelper->expects($this->once())
->method('send');
// Means no erros.
$this->userModel->emailUser($this->user, $subject, $content);
}
public function testSendMailToEmailAddresses(): void
{
$toMails = ['a@test.com', 'b@test.com'];
$subject = 'subject';
$content = 'content';
$this->mailHelper->expects($this->once())
->method('getMailer')
->willReturn($this->mailHelper);
$this->mailHelper->expects($this->once())
->method('setTo')
->with($toMails)
->willReturn(true);
$this->mailHelper->expects($this->once())
->method('send');
// Means no erros.
$this->userModel->sendMailToEmailAddresses($toMails, $subject, $content);
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Model\UserToken;
use Mautic\CoreBundle\Helper\RandomHelper\RandomHelperInterface;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Entity\UserToken;
use Mautic\UserBundle\Entity\UserTokenRepositoryInterface;
use Mautic\UserBundle\Model\UserToken\UserTokenService;
use PHPUnit\Framework\MockObject\MockObject;
class UserTokenServiceTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|RandomHelperInterface
*/
private MockObject $randomHelperMock;
/**
* @var MockObject|UserTokenRepositoryInterface
*/
private MockObject $userTokenRepositoryMock;
protected function setUp(): void
{
$this->randomHelperMock = $this->createMock(RandomHelperInterface::class);
$this->userTokenRepositoryMock = $this->createMock(UserTokenRepositoryInterface::class);
}
/**
* Tests second attempt for generating secret if not unique secret was generated first time.
*/
public function testGenerateSecret(): void
{
$secretLength = 6;
$randomSecret = 'secret';
$token = new UserToken();
$token->setAuthorizator('test-secret');
$this->randomHelperMock->expects($this->exactly(2))
->method('generate')
->with($secretLength)
->willReturn($randomSecret);
$this->userTokenRepositoryMock->expects($this->exactly(2))
->method('isSecretUnique')
->with($randomSecret)
->willReturnOnConsecutiveCalls(
false, // Test second attempt to get unique secret
true // Ok now
);
$userTokenService = $this->getUserTokenService();
$secretToken = $userTokenService->generateSecret($token, $secretLength);
$this->assertSame($randomSecret, $secretToken->getSecret());
$this->assertTrue($secretToken->isOneTimeOnly());
$this->assertNull($secretToken->getExpiration());
}
public function testVerify(): void
{
$token = new UserToken();
$user = new User();
$authorizator = 'authorizator';
$token->setUser($user)
->setOneTimeOnly(true)
->setExpiration(null)
->setAuthorizator($authorizator);
$this->userTokenRepositoryMock->expects($this->once())
->method('verify')
->with($token)
->willReturn(true);
$this->assertTrue($this->getUserTokenService()->verify($token));
}
private function getUserTokenService(): UserTokenService
{
return new UserTokenService(
$this->randomHelperMock,
$this->userTokenRepositoryMock
);
}
}

View File

@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security\Authenticator;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractSsoServiceIntegration;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Event\AuthenticationEvent;
use Mautic\UserBundle\Security\Authentication\AuthenticationHandler;
use Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions;
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PluginBadge;
use Mautic\UserBundle\Security\Authenticator\PluginAuthenticator;
use Mautic\UserBundle\UserEvents;
use OAuth2\OAuth2;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
class PluginAuthenticatorTest extends TestCase
{
public function testAuthenticateByPreAuthenticationReplacesToken(): void
{
$firewallName = 'main';
$integration = 'the integration';
$authenticatedIntegration = 'Auth integration';
$userIdentifier = 'some identifier';
$request = new Request(['integration' => $integration]);
$pluginToken = new PluginToken($firewallName, $integration);
$userProvider = $this->createMock(UserProviderInterface::class);
$integrationService = $this->createMock(AbstractSsoServiceIntegration::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->expects($this->once())
->method('getIntegrationObjects')
->with($integration, ['sso_service'], false, null, true)
->willReturn([$integrationService]);
$authEvent = new AuthenticationEvent(
null,
$pluginToken,
$userProvider,
$request,
false, // because there is no request attributes
$integration,
[$integrationService]
);
// If there will be an issue with this, then please replace with proper class name.
// I'm not 100% sure the SSO will return a User instance.
$authenticatedUser = $this->createMock(User::class);
$authenticatedUser->method('getUserIdentifier')->willReturn($userIdentifier);
$returnedPluginToken = new PluginToken($firewallName, $authenticatedIntegration);
$returnedPluginToken->setUser($authenticatedUser);
$returnedAuthEvent = clone $authEvent;
// Change token. Note this also changes authenticated integration and sets user.
$returnedAuthEvent->setToken($authenticatedIntegration, $returnedPluginToken);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->expects($this->once())
->method('hasListeners')
->with(UserEvents::USER_PRE_AUTHENTICATION)
->willReturn(true);
$dispatcher->expects($this->once())
->method('dispatch')
->with($authEvent)
->willReturn($returnedAuthEvent);
$authenticateResult = new PluginAuthenticator(
$this->createMock(TokenPermissions::class),
$dispatcher,
$integrationHelper,
$userProvider,
$this->createMock(AuthenticationHandler::class),
$this->createMock(OAuth2::class),
$this->createMock(LoggerInterface::class),
$firewallName
);
$authenticateResult = $authenticateResult->authenticate($request);
\assert($authenticateResult instanceof SelfValidatingPassport);
self::assertCount(2, $authenticateResult->getBadges());
$userBadge = $authenticateResult->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($userIdentifier, $userBadge->getUserIdentifier());
self::assertSame($authenticatedUser, $userBadge->getUser());
$pluginBadge = $authenticateResult->getBadge(PluginBadge::class);
\assert($pluginBadge instanceof PluginBadge);
self::assertSame($returnedPluginToken, $pluginBadge->getPreAuthenticatedToken());
self::assertSame($authenticatedIntegration, $pluginBadge->getAuthenticatingService());
}
public function testAuthenticateByPreAuthenticationSameToken(): void
{
$firewallName = 'main';
$integration = 'the integration';
$authenticatedIntegration = 'Auth integration';
$userIdentifier = 'some identifier';
$request = new Request(['integration' => $integration]);
$pluginToken = new PluginToken($firewallName, $integration);
$userProvider = $this->createMock(UserProviderInterface::class);
$integrationService = $this->createMock(AbstractSsoServiceIntegration::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->expects($this->once())
->method('getIntegrationObjects')
->with($integration, ['sso_service'], false, null, true)
->willReturn([$integrationService]);
$authEvent = new AuthenticationEvent(
null,
$pluginToken,
$userProvider,
$request,
false, // because there is no request attributes
$integration,
[$integrationService]
);
// If there will be an issue with this, then please replace with proper class name.
// I'm not 100% sure the SSO will return a User instance.
$authenticatedUser = $this->createMock(User::class);
$authenticatedUser->method('getUserIdentifier')->willReturn($userIdentifier);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->expects($this->once())
->method('hasListeners')
->with(UserEvents::USER_PRE_AUTHENTICATION)
->willReturn(true);
$dispatcher->expects($this->once())
->method('dispatch')
->with($authEvent)
->willReturnCallback(static function (AuthenticationEvent $event) use ($authenticatedIntegration, $authenticatedUser): AuthenticationEvent {
$event->setIsAuthenticated($authenticatedIntegration, $authenticatedUser, false);
$event->getToken()->setUser($authenticatedUser);
return $event;
});
$pluginAuthenticator = new PluginAuthenticator(
$this->createMock(TokenPermissions::class),
$dispatcher,
$integrationHelper,
$userProvider,
$this->createMock(AuthenticationHandler::class),
$this->createMock(OAuth2::class),
$this->createMock(LoggerInterface::class),
$firewallName
);
$authenticateResult = $pluginAuthenticator->authenticate($request);
\assert($authenticateResult instanceof SelfValidatingPassport);
self::assertCount(2, $authenticateResult->getBadges());
$userBadge = $authenticateResult->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($userIdentifier, $userBadge->getUserIdentifier());
self::assertSame($authenticatedUser, $userBadge->getUser());
$pluginBadge = $authenticateResult->getBadge(PluginBadge::class);
\assert($pluginBadge instanceof PluginBadge);
self::assertEquals(new PluginToken($firewallName, $integration, $authenticatedUser), $pluginBadge->getPreAuthenticatedToken());
self::assertSame($authenticatedIntegration, $pluginBadge->getAuthenticatingService());
}
public function testCreateTokenHasToken(): void
{
$firewallName = 'test';
$authenticatingService = 'Auth service';
$encodedPassword = 'En pass.';
$roles = ['role', 'the', 'roly'];
$pluginResponse = new Response();
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->expects(self::never())->method('hasListeners');
$dispatcher->expects(self::never())->method('dispatch');
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->expects(self::never())->method('getIntegrationObjects');
$userProvider = $this->createMock(UserProviderInterface::class);
$passportUser = $this->createMock(User::class);
$passportUser->method('getPassword')->willReturn($encodedPassword);
$passportUser->method('getRoles')->willReturn($roles);
$userBadge = new UserBadge('', function () use ($passportUser): UserInterface {
return $passportUser;
});
$pluginBadge = new PluginBadge(null, $pluginResponse, $authenticatingService);
$passport = new SelfValidatingPassport(
$userBadge,
[$pluginBadge],
);
$pluginToken = new PluginToken(
$firewallName,
$authenticatingService,
$passportUser,
$encodedPassword,
$roles,
$pluginBadge->getPluginResponse()
);
$tokenPermissions = $this->createMock(TokenPermissions::class);
$tokenPermissions->expects(self::once())
->method('setActivePermissionsOnAuthToken')
->with()
->willReturn($passportUser);
$pluginAuthenticator = new PluginAuthenticator(
$tokenPermissions,
$dispatcher,
$integrationHelper,
$userProvider,
$this->createMock(AuthenticationHandler::class),
$this->createMock(OAuth2::class),
$this->createMock(LoggerInterface::class),
$firewallName
);
self::assertEquals($pluginToken, $pluginAuthenticator->createToken($passport, $firewallName));
}
public function testHappyPathAuthenticationSuccess(): void
{
$firewallName = 'test';
$request = new Request();
$response = new Response();
$token = new PluginToken(null);
$authenticationHandler = $this->createMock(AuthenticationHandler::class);
$authenticationHandler->expects(self::once())
->method('onAuthenticationSuccess')
->with($request, $token)
->willReturn($response);
$session = $this->createMock(SessionInterface::class);
$session->expects(self::once())
->method('remove')
->with(SecurityRequestAttributes::AUTHENTICATION_ERROR);
$request->setSession($session);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher->expects(self::once())
->method('dispatch')
->with(
new InteractiveLoginEvent($request, $token),
SecurityEvents::INTERACTIVE_LOGIN
)
->willReturnArgument(0);
$pluginAuthenticator = new PluginAuthenticator(
$this->createMock(TokenPermissions::class),
$dispatcher,
$this->createMock(IntegrationHelper::class),
$this->createMock(UserProviderInterface::class),
$authenticationHandler,
$this->createMock(OAuth2::class),
$this->createMock(LoggerInterface::class),
$firewallName
);
self::assertSame($response, $pluginAuthenticator->onAuthenticationSuccess($request, $token, $firewallName));
}
public function testHappyPathAuthenticationFailure(): void
{
$firewallName = 'test';
$request = new Request();
$response = new Response();
$exception = $this->createMock(AuthenticationException::class);
$authenticationHandler = $this->createMock(AuthenticationHandler::class);
$authenticationHandler->expects(self::once())
->method('onAuthenticationFailure')
->with($request, $exception)
->willReturn($response);
$pluginAuthenticator = new PluginAuthenticator(
$this->createMock(TokenPermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(IntegrationHelper::class),
$this->createMock(UserProviderInterface::class),
$authenticationHandler,
$this->createMock(OAuth2::class),
$this->createMock(LoggerInterface::class),
$firewallName
);
self::assertSame($response, $pluginAuthenticator->onAuthenticationFailure($request, $exception));
}
}

View File

@@ -0,0 +1,593 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security\Authenticator;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractSsoServiceIntegration;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Event\AuthenticationEvent;
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PasswordStrengthBadge;
use Mautic\UserBundle\Security\Authenticator\SsoAuthenticator;
use Mautic\UserBundle\Security\Provider\UserProvider;
use Mautic\UserBundle\UserEvents;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
class SsoAuthenticatorTest extends TestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('provideIsPost')]
public function testIsPost(string $method, bool $isPost, bool $expected): void
{
$path = '/path';
$options = ['post_only' => $isPost, 'check_path' => $path, 'form_only' => false];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProviderInterface::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->server->set('REQUEST_METHOD', $method);
if (true === $expected) {
$httpUtils->method('checkRequestPath')
->with($request, $path)
->willReturn(true);
if ($isPost) {
$request->request->set('integration', 'integration');
} else {
$request->query->set('integration', 'integration');
}
}
self::assertSame($expected, $authenticator->supports($request));
}
public static function provideIsPost(): \Generator
{
yield 'is not POST and POST only' => [Request::METHOD_GET, true, false];
yield 'is POST and POST only' => [Request::METHOD_POST, true, true];
yield 'is not POST and not POST only' => [Request::METHOD_GET, false, true];
yield 'is POST and not POST only' => [Request::METHOD_POST, false, true];
}
#[\PHPUnit\Framework\Attributes\DataProvider('provideCheckPath')]
public function testCheckPath(bool $expected): void
{
$path = '/path';
$options = ['post_only' => true, 'check_path' => $path, 'form_only' => false];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProviderInterface::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->server->set('REQUEST_METHOD', Request::METHOD_POST);
$request->request->set('integration', 'integration');
$httpUtils->expects(self::once())
->method('checkRequestPath')
->with($request, $path)
->willReturn($expected);
self::assertSame($expected, $authenticator->supports($request));
}
public static function provideCheckPath(): \Generator
{
yield 'Is correct path' => [true];
yield 'Is not correct path' => [false];
}
#[\PHPUnit\Framework\Attributes\DataProvider('provideFormOnly')]
public function testFormOnly(string $mimeType, bool $isForm, bool $expected): void
{
$path = '/path';
$options = ['post_only' => true, 'check_path' => $path, 'form_only' => $isForm];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProviderInterface::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->server->set('REQUEST_METHOD', Request::METHOD_POST);
$request->request->set('integration', 'integration');
$request->headers->set('CONTENT_TYPE', $mimeType);
$httpUtils->expects(self::once())
->method('checkRequestPath')
->with($request, $path)
->willReturn(true);
self::assertSame($expected, $authenticator->supports($request));
}
public static function provideFormOnly(): \Generator
{
yield 'is not form and form only' => ['application/json', true, false];
yield 'is form and form only' => ['application/x-www-form-urlencoded', true, true];
yield 'is not form and not form only' => ['application/json', false, true];
yield 'is form and not form only' => ['application/x-www-form-urlencoded', false, true];
}
#[\PHPUnit\Framework\Attributes\DataProvider('provideRequestIntegrationParameter')]
public function testHasRequestIntegrationParameter(?bool $addToPost, bool $isPost, bool $expected): void
{
$path = '/path';
$options = ['post_only' => $isPost, 'check_path' => $path, 'form_only' => false];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProviderInterface::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->server->set('REQUEST_METHOD', Request::METHOD_POST);
if (null !== $addToPost) {
if ($addToPost) {
$request->request->set('integration', 'integration');
} else {
$request->query->set('integration', 'integration');
}
}
$httpUtils->expects(self::once())
->method('checkRequestPath')
->with($request, $path)
->willReturn(true);
self::assertSame($expected, $authenticator->supports($request));
}
public static function provideRequestIntegrationParameter(): \Generator
{
yield 'has POST parameter and is POST only' => [true, true, true];
yield 'has no POST parameter and is POST only' => [false, true, false];
yield 'has GET parameter and is not POST only' => [false, false, true];
yield 'has POST parameter and is not POST only' => [true, false, true];
yield 'has no POST or GET parameter and is not POST only' => [null, false, false];
}
#[\PHPUnit\Framework\Attributes\DataProvider('provideEnableCsrf')]
public function testBadges(bool $enableCsrf): void
{
$username = 'mautic';
$password = 'pw';
$integration = 'integration';
$csrfToken = 'token';
$options = ['post_only' => true, 'enable_csrf' => $enableCsrf];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProviderInterface::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$session = $this->createMock(SessionInterface::class);
$session->expects(self::once())
->method('set')
->with(SecurityRequestAttributes::LAST_USERNAME, $username);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->setSession($session);
$request->request->set('_username', $username);
$request->request->set('_password', $password);
$request->request->set('integration', $integration);
$request->request->set('_csrf_token', $csrfToken);
$passport = $authenticator->authenticate($request);
$badges = $passport->getBadges();
self::assertCount($enableCsrf ? 4 : 3, $badges);
$userBadge = $passport->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($username, $userBadge->getUserIdentifier());
$passwordBadge = $passport->getBadge(PasswordCredentials::class);
\assert($passwordBadge instanceof PasswordCredentials);
self::assertSame($password, $passwordBadge->getPassword());
self::assertTrue($passport->hasBadge(RememberMeBadge::class));
// Badge will be added later by PasswordStrengthSubscriber
$passwordStrengthBadge = $passport->getBadge(PasswordStrengthBadge::class);
self::assertNull($passwordStrengthBadge);
if (!$enableCsrf) {
self::assertFalse($passport->hasBadge(CsrfTokenBadge::class));
return;
}
$csrfTokenBadge = $passport->getBadge(CsrfTokenBadge::class);
\assert($csrfTokenBadge instanceof CsrfTokenBadge);
self::assertSame($csrfToken, $csrfTokenBadge->getCsrfToken());
self::assertSame('authenticate', $csrfTokenBadge->getCsrfTokenId());
}
public static function provideEnableCsrf(): \Generator
{
yield 'enable csrf' => [true];
yield 'not enable csrf' => [false];
}
public function testAuthenticateDoesNotLoadFromProviderAndNoListenersReturnsNoUser(): void
{
$username = 'mautic';
$password = 'pw';
$integration = 'integration';
$csrfToken = 'token';
$options = ['post_only' => true];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProvider::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$session = $this->createMock(SessionInterface::class);
$session->expects(self::once())
->method('set')
->with(SecurityRequestAttributes::LAST_USERNAME, $username);
$integrations = [$this->createMock(AbstractSsoServiceIntegration::class)];
$integrationHelper->expects(self::once())
->method('getIntegrationObjects')
->with($integration, ['sso_form'], false, null, true)
->willReturn($integrations);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($username)
->willThrowException(new UserNotFoundException());
$dispatcher->expects(self::once())
->method('hasListeners')
->with(UserEvents::USER_FORM_AUTHENTICATION)
->willReturn(false);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->setSession($session);
$request->request->set('_username', $username);
$request->request->set('_password', $password);
$request->request->set('integration', $integration);
$request->request->set('_csrf_token', $csrfToken);
$passport = $authenticator->authenticate($request);
$userBadge = $passport->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($username, $userBadge->getUserIdentifier());
$this->expectException(UserNotFoundException::class);
$userBadge->getUser();
}
public function testAuthenticateLoadsFromProviderAndNoListenersReturnsUser(): void
{
$username = 'mautic';
$password = 'pw';
$integration = 'integration';
$csrfToken = 'token';
$options = ['post_only' => true];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProvider::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$session = $this->createMock(SessionInterface::class);
$session->expects(self::once())
->method('set')
->with(SecurityRequestAttributes::LAST_USERNAME, $username);
$integrations = [$this->createMock(AbstractSsoServiceIntegration::class)];
$integrationHelper->expects(self::once())
->method('getIntegrationObjects')
->with($integration, ['sso_form'], false, null, true)
->willReturn($integrations);
$user = $this->createMock(User::class);
$user->expects(self::once())
->method('getRoles')
->willReturn([]);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($username)
->willReturn($user);
$dispatcher->expects(self::once())
->method('hasListeners')
->with(UserEvents::USER_FORM_AUTHENTICATION)
->willReturn(false);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$request = new Request();
$request->setSession($session);
$request->request->set('_username', $username);
$request->request->set('_password', $password);
$request->request->set('integration', $integration);
$request->request->set('_csrf_token', $csrfToken);
$passport = $authenticator->authenticate($request);
$userBadge = $passport->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($username, $userBadge->getUserIdentifier());
self::assertSame($user, $userBadge->getUser());
}
public function testAuthenticateListenerForcesFailure(): void
{
$username = 'mautic';
$password = 'pw';
$integration = 'integration';
$csrfToken = 'token';
$userRoles = ['ROLE'];
$options = ['post_only' => true];
$failedMessage = 'Failure';
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProvider::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$session = $this->createMock(SessionInterface::class);
$session->expects(self::once())
->method('set')
->with(SecurityRequestAttributes::LAST_USERNAME, $username);
$integrations = [$this->createMock(AbstractSsoServiceIntegration::class)];
$integrationHelper->expects(self::once())
->method('getIntegrationObjects')
->with($integration, ['sso_form'], false, null, true)
->willReturn($integrations);
$user = $this->createMock(User::class);
$user->expects(self::once())
->method('getRoles')
->willReturn($userRoles);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($username)
->willReturn($user);
$request = new Request();
$request->setSession($session);
$request->request->set('_username', $username);
$request->request->set('_password', $password);
$request->request->set('integration', $integration);
$request->request->set('_csrf_token', $csrfToken);
$token = new PluginToken(
null,
$integration,
$username,
$password,
$userRoles,
);
$callEvent = new AuthenticationEvent(
$user,
$token,
$userProvider,
$request,
false,
$integration,
$integrations
);
$returnEvent = clone $callEvent;
$returnEvent->setFailedAuthenticationMessage($failedMessage);
$returnEvent->setIsFailedAuthentication();
$dispatcher->expects(self::once())
->method('hasListeners')
->with(UserEvents::USER_FORM_AUTHENTICATION)
->willReturn(true);
$dispatcher->expects(self::once())
->method('dispatch')
->with($callEvent, UserEvents::USER_FORM_AUTHENTICATION)
->willReturn($returnEvent);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$passport = $authenticator->authenticate($request);
$userBadge = $passport->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($username, $userBadge->getUserIdentifier());
$this->expectException(AuthenticationException::class);
$this->expectExceptionMessage($failedMessage);
$userBadge->getUser();
}
public function testAuthenticateListenerLoadsUser(): void
{
$username = 'mautic';
$password = 'pw';
$integration = 'integration';
$csrfToken = 'token';
$options = ['post_only' => true];
$httpUtils = $this->createMock(HttpUtils::class);
$userProvider = $this->createMock(UserProvider::class);
$successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$session = $this->createMock(SessionInterface::class);
$session->expects(self::once())
->method('set')
->with(SecurityRequestAttributes::LAST_USERNAME, $username);
$integrations = [$this->createMock(AbstractSsoServiceIntegration::class)];
$integrationHelper->expects(self::once())
->method('getIntegrationObjects')
->with($integration, ['sso_form'], false, null, true)
->willReturn($integrations);
$user = $this->createMock(User::class);
$userProvider->expects(self::once())
->method('loadUserByIdentifier')
->with($username)
->willThrowException(new UserNotFoundException());
$request = new Request();
$request->setSession($session);
$request->request->set('_username', $username);
$request->request->set('_password', $password);
$request->request->set('integration', $integration);
$request->request->set('_csrf_token', $csrfToken);
$token = new PluginToken(
null,
$integration,
$username,
'',
[],
);
$callEvent = new AuthenticationEvent(
$username,
$token,
$userProvider,
$request,
false,
$integration,
$integrations
);
$returnEvent = clone $callEvent;
$returnEvent->setIsAuthenticated($integration, $user, false);
$dispatcher->expects(self::once())
->method('hasListeners')
->with(UserEvents::USER_FORM_AUTHENTICATION)
->willReturn(true);
$dispatcher->expects(self::once())
->method('dispatch')
->with($callEvent, UserEvents::USER_FORM_AUTHENTICATION)
->willReturn($returnEvent);
$authenticator = new SsoAuthenticator(
$options,
$httpUtils,
$userProvider,
$successHandler,
$failureHandler,
$integrationHelper,
$dispatcher
);
$passport = $authenticator->authenticate($request);
$userBadge = $passport->getBadge(UserBadge::class);
\assert($userBadge instanceof UserBadge);
self::assertSame($username, $userBadge->getUserIdentifier());
self::assertSame($user, $userBadge->getUser());
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security\Authenticator;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Security\TimingSafeFormLoginAuthenticator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
class TimingSafeFormLoginAuthenticatorTest extends TestCase
{
/**
* @return array<mixed>
*/
private function getCredentials(TimingSafeFormLoginAuthenticator $authenticator, Request $request): array
{
$method = new \ReflectionMethod(TimingSafeFormLoginAuthenticator::class, 'getCredentials');
$method->setAccessible(true);
return $method->invoke($authenticator, $request);
}
public function testAuthenticateWithExistingUser(): void
{
$request = new Request([], ['username' => 'testuser', 'password' => 'password']);
$request->setSession(new Session(new MockArraySessionStorage()));
$user = new User();
$user->setUsername('testuser');
/** @var UserProviderInterface|\PHPUnit\Framework\MockObject\MockObject $userProvider */
$userProvider = $this->createMock(UserProviderInterface::class);
$userProvider->expects($this->once())
->method('loadUserByIdentifier')
->with('testuser')
->willReturn($user);
$passwordHasher = $this->createMock(PasswordHasherInterface::class);
/** @var PasswordHasherFactoryInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasherFactory */
$passwordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
$passwordHasherFactory->expects($this->never())
->method('getPasswordHasher');
/** @var FormLoginAuthenticator|\PHPUnit\Framework\MockObject\MockObject $formLoginAuthenticator */
$formLoginAuthenticator = $this->createMock(FormLoginAuthenticator::class);
$authenticator = new TimingSafeFormLoginAuthenticator(
$formLoginAuthenticator,
$userProvider,
$passwordHasherFactory,
[
'enable_csrf' => false,
'username_parameter' => 'username',
'password_parameter' => 'password',
'csrf_parameter' => '_csrf_token',
'post_only' => true,
]
);
$credentials = $this->getCredentials($authenticator, $request);
$this->assertEquals('testuser', $credentials['username']);
$this->assertEquals('password', $credentials['password']);
$passport = $authenticator->authenticate($request);
$passport->getUser();
}
public function testAuthenticateWithNonExistingUser(): void
{
$this->expectException(UserNotFoundException::class);
$request = new Request([], ['username' => 'testuser', 'password' => 'password']);
$request->setSession(new Session(new MockArraySessionStorage()));
/** @var UserProviderInterface|\PHPUnit\Framework\MockObject\MockObject $userProvider */
$userProvider = $this->createMock(UserProviderInterface::class);
$userProvider->expects($this->once())
->method('loadUserByIdentifier')
->with('testuser')
->willThrowException(new UserNotFoundException());
/** @var PasswordHasherInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasher */
$passwordHasher = $this->createMock(PasswordHasherInterface::class);
$passwordHasher->expects($this->once())
->method('verify')
->with('$2y$13$aAwXNyqA87lcXQQuk8Cp6eo2amRywLct29oG2uWZ8lYBeamFZ8UhK', 'password');
/** @var PasswordHasherFactoryInterface|\PHPUnit\Framework\MockObject\MockObject $passwordHasherFactory */
$passwordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class);
$passwordHasherFactory->expects($this->once())
->method('getPasswordHasher')
->willReturn($passwordHasher);
/** @var FormLoginAuthenticator|\PHPUnit\Framework\MockObject\MockObject $formLoginAuthenticator */
$formLoginAuthenticator = $this->createMock(FormLoginAuthenticator::class);
$authenticator = new TimingSafeFormLoginAuthenticator(
$formLoginAuthenticator,
$userProvider,
$passwordHasherFactory,
[
'enable_csrf' => false,
'username_parameter' => 'username',
'password_parameter' => 'password',
'csrf_parameter' => '_csrf_token',
'post_only' => true,
]
);
$passport = $authenticator->authenticate($request);
$passport->getUser();
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security\SAML;
use LightSaml\Credential\X509Certificate;
use LightSaml\Credential\X509Credential;
use LightSaml\Store\Credential\CredentialStoreInterface;
use Mautic\UserBundle\Security\SAML\EntityDescriptorProviderFactory;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\RouterInterface;
class EntityDescriptorProviderFactoryTest extends TestCase
{
public function testBuild(): void
{
$router = $this->createMock(RouterInterface::class);
$credentialStore = $this->createMock(CredentialStoreInterface::class);
$entityId = 'https://example.com';
$samlRoute = '/saml/login';
$router->expects($this->once())
->method('generate')
->with($samlRoute)
->willReturn($samlRoute);
$credentialStore->expects($this->once())
->method('getByEntityId')
->with($entityId)
->willReturn([$credential = $this->createMock(X509Credential::class)]);
$credential->expects($this->once())
->method('getCertificate')
->willReturn(new X509Certificate());
$builder = EntityDescriptorProviderFactory::build(
$entityId,
$router,
$samlRoute,
$credentialStore
);
$entityDescriptor = $builder->get();
Assert::assertCount(
1,
$entityDescriptor->getFirstSpSsoDescriptor()->getAllAssertionConsumerServicesByUrl('https://example.com/saml/login'),
'When building the SpSsoDescriptor, it should add a single AssertionConsumerService with the correct url. '
);
Assert::assertEquals(
$entityId,
$entityDescriptor->getEntityID(),
'The entity ID should be set to the passed entity ID'
);
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Mautic\UserBundle\Tests\Security\SAML\Store;
use LightSaml\Credential\X509Credential;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\UserBundle\Security\SAML\Store\CredentialsStore;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class CredentialsStoreTest extends TestCase
{
private string $cacheDir;
/**
* @var CoreParametersHelper|MockObject
*/
private MockObject $coreParametersHelper;
protected function setUp(): void
{
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->cacheDir = dirname((new \ReflectionClass(\Composer\Autoload\ClassLoader::class))->getFileName(), 3);
}
public function testEmptyArrayReturnedIfEntityIdsDoNotMatch(): void
{
$store = new CredentialsStore($this->coreParametersHelper, 'foobar');
$this->assertEquals([], $store->getByEntityId('barfoo'));
}
public function testDefaultCredentialsAreUsedIfSamlIsDisabled(): void
{
$matcher = $this->exactly(2);
$this->coreParametersHelper->expects($matcher)->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_metadata', $parameters[0]);
return '';
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('cache_path', $parameters[0]);
return $this->cacheDir;
}
});
$store = new CredentialsStore($this->coreParametersHelper, 'foobar');
$credentials = $store->getByEntityId('foobar');
$this->assertCount(1, $credentials);
$this->assertInstanceOf(X509Credential::class, $credentials[0]);
}
public function testDefaultCredentialsAreUsedIfCustomCertificateIsNotProvided(): void
{
$matcher = $this->exactly(3);
$this->coreParametersHelper->expects($matcher)->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_metadata', $parameters[0]);
return '1';
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_own_certificate', $parameters[0]);
return '';
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertSame('cache_path', $parameters[0]);
return $this->cacheDir;
}
});
$store = new CredentialsStore($this->coreParametersHelper, 'foobar');
$credentials = $store->getByEntityId('foobar');
$this->assertCount(1, $credentials);
$this->assertInstanceOf(X509Credential::class, $credentials[0]);
}
public function testOwnCredentialsAreUsedIfProvided(): void
{
$matcher = $this->exactly(5);
$this->coreParametersHelper->expects($matcher)->method('get')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_metadata', $parameters[0]);
return '1';
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_own_certificate', $parameters[0]);
return 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNOakNDQVorZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREE0TVFzd0NRWURWUVFHRXdKMWN6RUwKTUFrR0ExVUVDQXdDVkZneERUQUxCZ05WQkFvTUJGUmxjM1F4RFRBTEJnTlZCQU1NQkZSbGMzUXdIaGNOTVRreApNakk1TVRjME56RTBXaGNOTWpBeE1qSTRNVGMwTnpFMFdqQTRNUXN3Q1FZRFZRUUdFd0oxY3pFTE1Ba0dBMVVFCkNBd0NWRmd4RFRBTEJnTlZCQW9NQkZSbGMzUXhEVEFMQmdOVkJBTU1CRlJsYzNRd2daOHdEUVlKS29aSWh2Y04KQVFFQkJRQURnWTBBTUlHSkFvR0JBTDQ4eCtJY29BQVVjOVEvL2QxRkhxZFQ1WjNWejRCSVIzNFJqNUUvQkpkegpmODN0dGx0NnBKNFdCbEFYcFlHWW5PSDh4YXpjdGJEUzd2QVVhbmtQMUxBV2haUnBDeFVkdHg2VlV3MXZlNS8xCnRjV1VBcnBZdFVIMXJHdEdoaDlncFJMVkxEMktxaWQzengyMjlXeHJmaHV0NjVBbEJKRzlSeVV6T2E4cWlVS2IKQWdNQkFBR2pVREJPTUIwR0ExVWREZ1FXQkJUZWtkN0RvWUI4dFc0K2N3TGYzR0FKNTl5VFVEQWZCZ05WSFNNRQpHREFXZ0JUZWtkN0RvWUI4dFc0K2N3TGYzR0FKNTl5VFVEQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzCkRRRUJEUVVBQTRHQkFGd05Uc3lHNVZ5dG5EdWF5ZjBmbi9zOGtPcG1mcG1FcDBTRDFBajdvRGhNTytHdG5SWGEKUGZsWVozWlFJWCt4Wkl2K1FSOTNZNUZDM1h2V1JWbk9abWtybzh3YmZoZkFOa2ZGWnFiNFg3SlFqY2YrOVNOTwoxenpyVVVKK1BSVGpBSnR3REdrRVB6Q2d3UDk5QVIrUm5UQ1RaUS9OM2xoQXl3Zm1qRTNQNUpoNwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t';
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_own_certificate', $parameters[0]);
return 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNOakNDQVorZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRMEZBREE0TVFzd0NRWURWUVFHRXdKMWN6RUwKTUFrR0ExVUVDQXdDVkZneERUQUxCZ05WQkFvTUJGUmxjM1F4RFRBTEJnTlZCQU1NQkZSbGMzUXdIaGNOTVRreApNakk1TVRjME56RTBXaGNOTWpBeE1qSTRNVGMwTnpFMFdqQTRNUXN3Q1FZRFZRUUdFd0oxY3pFTE1Ba0dBMVVFCkNBd0NWRmd4RFRBTEJnTlZCQW9NQkZSbGMzUXhEVEFMQmdOVkJBTU1CRlJsYzNRd2daOHdEUVlKS29aSWh2Y04KQVFFQkJRQURnWTBBTUlHSkFvR0JBTDQ4eCtJY29BQVVjOVEvL2QxRkhxZFQ1WjNWejRCSVIzNFJqNUUvQkpkegpmODN0dGx0NnBKNFdCbEFYcFlHWW5PSDh4YXpjdGJEUzd2QVVhbmtQMUxBV2haUnBDeFVkdHg2VlV3MXZlNS8xCnRjV1VBcnBZdFVIMXJHdEdoaDlncFJMVkxEMktxaWQzengyMjlXeHJmaHV0NjVBbEJKRzlSeVV6T2E4cWlVS2IKQWdNQkFBR2pVREJPTUIwR0ExVWREZ1FXQkJUZWtkN0RvWUI4dFc0K2N3TGYzR0FKNTl5VFVEQWZCZ05WSFNNRQpHREFXZ0JUZWtkN0RvWUI4dFc0K2N3TGYzR0FKNTl5VFVEQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzCkRRRUJEUVVBQTRHQkFGd05Uc3lHNVZ5dG5EdWF5ZjBmbi9zOGtPcG1mcG1FcDBTRDFBajdvRGhNTytHdG5SWGEKUGZsWVozWlFJWCt4Wkl2K1FSOTNZNUZDM1h2V1JWbk9abWtybzh3YmZoZkFOa2ZGWnFiNFg3SlFqY2YrOVNOTwoxenpyVVVKK1BSVGpBSnR3REdrRVB6Q2d3UDk5QVIrUm5UQ1RaUS9OM2xoQXl3Zm1qRTNQNUpoNwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t';
}
if (4 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_own_private_key', $parameters[0]);
return 'LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLQpNSUlDeGpCQUJna3Foa2lHOXcwQkJRMHdNekFiQmdrcWhraUc5dzBCQlF3d0RnUUkrdGdUM1FGaGpFZ0NBZ2dBCk1CUUdDQ3FHU0liM0RRTUhCQWcxSEdyU0hiN3pWd1NDQW9CRzg5enFBeEF4K3ZQdmhRVlc2ZGZKRkJUU3BBR3EKUlJsZml5Z3IwaXdMQ3hwbm9UNVpZLzBPZDI4dXZCL0hrQWIwY0NnNnNZdk5WdkRERGl0c3BDYjN2SVUvZ1BtTgpoOFZFdGd4RGxYRUUxWXdpbkRuMEJmTzV0Um9DNFNmT3JKUnNyUmRYNHFjN3hrODBLazc0Y2J6TEp5NlZFU1E1CnUvWlk4WXcyRTlleElINHJsWitkQmZzNEpla1FTL2ZMYmlYR2R0U2RWRjZSTzBlOUloUnNiM2RVaFNxcUphZmwKNitWa3B0aGVYR0Z3VHlTZjhjNXlMbU1VQy96Mk5DR3hPNUc5MXVVeHNoRVF2ZFEyTk0wa3Z0OUFHM1Rjd1o2ZwpvYnFqSFdmVkVtVDlqOVJhcWJrbis5ZGVzYWxLUk9OYmUxbEkwSUwxdmNJQlhXZHVRVU5RSzFkVDlnaGJ3R1QrCnZlZ3c4NnhsTXRMTlBydkZ4YzNHNlpWdGlkL1Qxd0RYWkVLS0NxcDlVZWkzZmgwU0xLeTJ3aXRqdDV5dmFiYmMKUWhYT1MyaFZBOXpTUTBJR2N5Y1d6eGVhZitOYjgyNnh2dkFWOVRmMUdocmVUNnZRV0M4QnhEQ1ZyOXczMWg2eQpMbkM1UjJkeHpvbTFkL2tpQnFsY0doMXU5ZCsyT3lDRnBmWXZ5bVdsS2NYWVlPOEUyTnUyb0s0SWx6QjBZZEpwCjgveTdQTEd6YWlwZjU3ZThzckdNR3ZMTUt3UVRMQ3ZhRHUvZ3hsNGQrYXd3VFo5VXNHMnFNT0taU2tJVTRJTXUKdVAwQ0RTYlJ4YkhCcTlnWTRaaUVDdUF4bW1vYWRaWG1OT3U4aUdRV2E5cm1vQ3FXNVhtQWd3dmNuZnhEV2F3SQpaVEpVZ0hkWjZUZmUyalBGSmpTWlZvVS9lbjBXNUJRWGd5MXUzQkRYNjhDOG5BZlo0eG1leUVMY011YjloVFliCkhURG1ZSUJvelpOSWNZSEI2T0dablVSdUdlb2ZNVkpxTWtOZm5FdVN1b0NKc1hHSWhMem5ES3A4RzAwRjllUjIKMUwrQjBaVVp2L084MnFFR3pDL0lYNytDRm1TRFN0VjlSNDAwY0R2aSs4QnNkTU1CK1dWNlNNbksKLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0=';
}
if (5 === $matcher->numberOfInvocations()) {
$this->assertSame('saml_idp_own_password', $parameters[0]);
return 'abc123';
}
});
$store = new CredentialsStore($this->coreParametersHelper, 'foobar');
$credentials = $store->getByEntityId('foobar');
$this->assertCount(1, $credentials);
$cert = $credentials[0];
$this->assertInstanceOf(X509Credential::class, $cert);
$issuer = $cert->getCertificate()->getIssuer();
$this->assertEquals('TX', $issuer['ST']);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Mautic\UserBundle\Tests\Security\SAML\Store;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\UserBundle\Security\SAML\Store\EntityDescriptorStore;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class EntityDescriptorStoreTest extends TestCase
{
/**
* @var CoreParametersHelper|MockObject
*/
private MockObject $coreParametersHelper;
protected function setUp(): void
{
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
}
public function testNullIsReturnedIfEntityIdDoesNotMatch(): void
{
$store = new EntityDescriptorStore($this->coreParametersHelper);
$this->coreParametersHelper->method('get')
->with('saml_idp_metadata')
->willReturn(
'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48bWQ6RW50aXR5RGVzY3JpcHRvciB4bWxuczptZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm1ldGFkYXRhIiBlbnRpdHlJRD0iaHR0cHM6Ly9tYXV0aWMtZGV2LWVkLm15LnNhbGVzZm9yY2UuY29tIiB2YWxpZFVudGlsPSIyMDI5LTEyLTI4VDE0OjUyOjA2LjIyMFoiIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgPG1kOklEUFNTT0Rlc2NyaXB0b3IgcHJvdG9jb2xTdXBwb3J0RW51bWVyYXRpb249InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+CiAgICAgIDxtZDpLZXlEZXNjcmlwdG9yIHVzZT0ic2lnbmluZyI+CiAgICAgICAgIDxkczpLZXlJbmZvPgogICAgICAgICAgICA8ZHM6WDUwOURhdGE+CiAgICAgICAgICAgICAgIDxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRVpEQ0NBMHlnQXdJQkFnSU9BVzlNNS9Nb0FBQUFBRUpEc2Vjd0RRWUpLb1pJaHZjTkFRRUxCUUF3ZWpFU01CQUdBMVVFQXd3SlUwRk5URjkwWlhOME1SZ3dGZ1lEVlFRTERBOHdNRVJxTURBd01EQXdNRXd3U1RreEZ6QVZCZ05WQkFvTURsTmhiR1Z6Wm05eVkyVXVZMjl0TVJZd0ZBWURWUVFIREExVFlXNGdSbkpoYm1OcGMyTnZNUXN3Q1FZRFZRUUlEQUpEUVRFTU1Bb0dBMVVFQmhNRFZWTkJNQjRYRFRFNU1USXlPREUwTWpjME4xb1hEVEl3TVRJeU9ERXlNREF3TUZvd2VqRVNNQkFHQTFVRUF3d0pVMEZOVEY5MFpYTjBNUmd3RmdZRFZRUUxEQTh3TUVScU1EQXdNREF3TUV3d1NUa3hGekFWQmdOVkJBb01EbE5oYkdWelptOXlZMlV1WTI5dE1SWXdGQVlEVlFRSERBMVRZVzRnUm5KaGJtTnBjMk52TVFzd0NRWURWUVFJREFKRFFURU1NQW9HQTFVRUJoTURWVk5CTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFoVVBxVEoyQ3YreVhPYzcwaW13d05IWE44OTBzQzliU1FsU05MbnJ6cHN5MFB4R0paQmRuL3hIWVlVS2FUZWxvMytHOXRGL1BIQkdHQlMrMGZPN0Rjd254KzVKRnhUQW1MR0ptdnBTN2UrdWc0T2F1SDNidWQ0ck9kbnVzNTczUjd5SjNPZi9IT25DTEpNN3R4TGxaMUorZmUxT2FkOVhHK1dWZGIvL1U0UzBqU09Lb1c5QVlxQjlPd0pLak1aNm9GWXFnQnltZzBiRS9YRFZyTHZZcktNMEkwaEpUQzQ2R1pVc1ZJZUZGM1lDVWtxcDhTZkYzWlFUZzF5SHltbjZiOHJvQjZYVy9yd3dUWVR5MFkwOFlYR0ltWEVseTVoTXFRQ25zc3BjNnJwa3VuUHlqSUY5TlV2NHBCeEU3SXhQcFFld0NrbjBGdVNIRVJQQUM5MVA5eHdJREFRQUJvNEhuTUlIa01CMEdBMVVkRGdRV0JCUlVFVnlKUSs2czdGUzhsM210R3V1ZmpMQXpnVEFQQmdOVkhSTUJBZjhFQlRBREFRSC9NSUd4QmdOVkhTTUVnYWt3Z2FhQUZGUVJYSWxEN3F6c1ZMeVhlYTBhNjUrTXNET0JvWDZrZkRCNk1SSXdFQVlEVlFRRERBbFRRVTFNWDNSbGMzUXhHREFXQmdOVkJBc01EekF3Ukdvd01EQXdNREF3VERCSk9URVhNQlVHQTFVRUNnd09VMkZzWlhObWIzSmpaUzVqYjIweEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4Q3pBSkJnTlZCQWdNQWtOQk1Rd3dDZ1lEVlFRR0V3TlZVMEdDRGdGdlRPZnpLQUFBQUFCQ1E3SG5NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUE4U3NDS3lMVXE5L25RYXpxK1B0N1RRWWpMaVBWMldOeVcxeEFGQWQxekFEcW5vR1ovZFRZNkQrdTVrZExuK3paUEptaXFuVGdad01Rc3AxdXJ3SmlaK3JncXg0R3hkRlhPakZRTTZnV2RjN0xuSTJxcTI1M2F4SHRaZFNuVTE5NDFWaEc5RXVSdDNIa2tLR3VOVGUwK05GTGJKYXR6Tk04bW80dGZ4Vkxub3NxWUFSTFEvaHVKUURYUUVhcE90ZUhxYkVJbE1OTjJGUi9hYk9lNTRlaWpSRmFncXJqWEtwMlVJTFh2NEFIcE5YVjI2ek43WVpOKzhJc1pPam9RYUtLYlB4MStwRWk1NzZvQlFSSUZ1N01sRkNsc3h0QW9DNmpPb1dCV01QbXR5UGxTNEdKWlRrY056UHJNbGxRem9uZWRGWDlvTk9ZRExiRnRlak1jOWlmWjwvZHM6WDUwOUNlcnRpZmljYXRlPgogICAgICAgICAgICA8L2RzOlg1MDlEYXRhPgogICAgICAgICA8L2RzOktleUluZm8+CiAgICAgIDwvbWQ6S2V5RGVzY3JpcHRvcj4KICAgICAgPG1kOlNpbmdsZUxvZ291dFNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbWF1dGljLWRldi1lZC5teS5zYWxlc2ZvcmNlLmNvbS9zZXJ2aWNlcy9hdXRoL2lkcC9zYW1sMi9sb2dvdXQiLz4KICAgICAgPG1kOlNpbmdsZUxvZ291dFNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUmVkaXJlY3QiIExvY2F0aW9uPSJodHRwczovL21hdXRpYy1kZXYtZWQubXkuc2FsZXNmb3JjZS5jb20vc2VydmljZXMvYXV0aC9pZHAvc2FtbDIvbG9nb3V0Ii8+CiAgICAgIDxtZDpOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQ8L21kOk5hbWVJREZvcm1hdD4KICAgICAgPG1kOlNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbWF1dGljLWRldi1lZC5teS5zYWxlc2ZvcmNlLmNvbS9pZHAvZW5kcG9pbnQvSHR0cFBvc3QiLz4KICAgICAgPG1kOlNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUmVkaXJlY3QiIExvY2F0aW9uPSJodHRwczovL21hdXRpYy1kZXYtZWQubXkuc2FsZXNmb3JjZS5jb20vaWRwL2VuZHBvaW50L0h0dHBSZWRpcmVjdCIvPgogICA8L21kOklEUFNTT0Rlc2NyaXB0b3I+CjwvbWQ6RW50aXR5RGVzY3JpcHRvcj4='
);
$descriptor = $store->get('foobar');
$this->assertNull($descriptor);
}
public function testHasReturnsFalseIfSamlIsDisabled(): void
{
$store = new EntityDescriptorStore($this->coreParametersHelper);
$this->coreParametersHelper->method('get')
->with('saml_idp_metadata')
->willReturn('');
$this->assertFalse($store->has('foobar'));
}
public function testHasReturnsFalseIfEntityIdDoesNotMatch(): void
{
$store = new EntityDescriptorStore($this->coreParametersHelper);
$this->coreParametersHelper->method('get')
->with('saml_idp_metadata')
->willReturn(
'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48bWQ6RW50aXR5RGVzY3JpcHRvciB4bWxuczptZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm1ldGFkYXRhIiBlbnRpdHlJRD0iaHR0cHM6Ly9tYXV0aWMtZGV2LWVkLm15LnNhbGVzZm9yY2UuY29tIiB2YWxpZFVudGlsPSIyMDI5LTEyLTI4VDE0OjUyOjA2LjIyMFoiIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgPG1kOklEUFNTT0Rlc2NyaXB0b3IgcHJvdG9jb2xTdXBwb3J0RW51bWVyYXRpb249InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+CiAgICAgIDxtZDpLZXlEZXNjcmlwdG9yIHVzZT0ic2lnbmluZyI+CiAgICAgICAgIDxkczpLZXlJbmZvPgogICAgICAgICAgICA8ZHM6WDUwOURhdGE+CiAgICAgICAgICAgICAgIDxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRVpEQ0NBMHlnQXdJQkFnSU9BVzlNNS9Nb0FBQUFBRUpEc2Vjd0RRWUpLb1pJaHZjTkFRRUxCUUF3ZWpFU01CQUdBMVVFQXd3SlUwRk5URjkwWlhOME1SZ3dGZ1lEVlFRTERBOHdNRVJxTURBd01EQXdNRXd3U1RreEZ6QVZCZ05WQkFvTURsTmhiR1Z6Wm05eVkyVXVZMjl0TVJZd0ZBWURWUVFIREExVFlXNGdSbkpoYm1OcGMyTnZNUXN3Q1FZRFZRUUlEQUpEUVRFTU1Bb0dBMVVFQmhNRFZWTkJNQjRYRFRFNU1USXlPREUwTWpjME4xb1hEVEl3TVRJeU9ERXlNREF3TUZvd2VqRVNNQkFHQTFVRUF3d0pVMEZOVEY5MFpYTjBNUmd3RmdZRFZRUUxEQTh3TUVScU1EQXdNREF3TUV3d1NUa3hGekFWQmdOVkJBb01EbE5oYkdWelptOXlZMlV1WTI5dE1SWXdGQVlEVlFRSERBMVRZVzRnUm5KaGJtTnBjMk52TVFzd0NRWURWUVFJREFKRFFURU1NQW9HQTFVRUJoTURWVk5CTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFoVVBxVEoyQ3YreVhPYzcwaW13d05IWE44OTBzQzliU1FsU05MbnJ6cHN5MFB4R0paQmRuL3hIWVlVS2FUZWxvMytHOXRGL1BIQkdHQlMrMGZPN0Rjd254KzVKRnhUQW1MR0ptdnBTN2UrdWc0T2F1SDNidWQ0ck9kbnVzNTczUjd5SjNPZi9IT25DTEpNN3R4TGxaMUorZmUxT2FkOVhHK1dWZGIvL1U0UzBqU09Lb1c5QVlxQjlPd0pLak1aNm9GWXFnQnltZzBiRS9YRFZyTHZZcktNMEkwaEpUQzQ2R1pVc1ZJZUZGM1lDVWtxcDhTZkYzWlFUZzF5SHltbjZiOHJvQjZYVy9yd3dUWVR5MFkwOFlYR0ltWEVseTVoTXFRQ25zc3BjNnJwa3VuUHlqSUY5TlV2NHBCeEU3SXhQcFFld0NrbjBGdVNIRVJQQUM5MVA5eHdJREFRQUJvNEhuTUlIa01CMEdBMVVkRGdRV0JCUlVFVnlKUSs2czdGUzhsM210R3V1ZmpMQXpnVEFQQmdOVkhSTUJBZjhFQlRBREFRSC9NSUd4QmdOVkhTTUVnYWt3Z2FhQUZGUVJYSWxEN3F6c1ZMeVhlYTBhNjUrTXNET0JvWDZrZkRCNk1SSXdFQVlEVlFRRERBbFRRVTFNWDNSbGMzUXhHREFXQmdOVkJBc01EekF3Ukdvd01EQXdNREF3VERCSk9URVhNQlVHQTFVRUNnd09VMkZzWlhObWIzSmpaUzVqYjIweEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4Q3pBSkJnTlZCQWdNQWtOQk1Rd3dDZ1lEVlFRR0V3TlZVMEdDRGdGdlRPZnpLQUFBQUFCQ1E3SG5NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUE4U3NDS3lMVXE5L25RYXpxK1B0N1RRWWpMaVBWMldOeVcxeEFGQWQxekFEcW5vR1ovZFRZNkQrdTVrZExuK3paUEptaXFuVGdad01Rc3AxdXJ3SmlaK3JncXg0R3hkRlhPakZRTTZnV2RjN0xuSTJxcTI1M2F4SHRaZFNuVTE5NDFWaEc5RXVSdDNIa2tLR3VOVGUwK05GTGJKYXR6Tk04bW80dGZ4Vkxub3NxWUFSTFEvaHVKUURYUUVhcE90ZUhxYkVJbE1OTjJGUi9hYk9lNTRlaWpSRmFncXJqWEtwMlVJTFh2NEFIcE5YVjI2ek43WVpOKzhJc1pPam9RYUtLYlB4MStwRWk1NzZvQlFSSUZ1N01sRkNsc3h0QW9DNmpPb1dCV01QbXR5UGxTNEdKWlRrY056UHJNbGxRem9uZWRGWDlvTk9ZRExiRnRlak1jOWlmWjwvZHM6WDUwOUNlcnRpZmljYXRlPgogICAgICAgICAgICA8L2RzOlg1MDlEYXRhPgogICAgICAgICA8L2RzOktleUluZm8+CiAgICAgIDwvbWQ6S2V5RGVzY3JpcHRvcj4KICAgICAgPG1kOlNpbmdsZUxvZ291dFNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbWF1dGljLWRldi1lZC5teS5zYWxlc2ZvcmNlLmNvbS9zZXJ2aWNlcy9hdXRoL2lkcC9zYW1sMi9sb2dvdXQiLz4KICAgICAgPG1kOlNpbmdsZUxvZ291dFNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUmVkaXJlY3QiIExvY2F0aW9uPSJodHRwczovL21hdXRpYy1kZXYtZWQubXkuc2FsZXNmb3JjZS5jb20vc2VydmljZXMvYXV0aC9pZHAvc2FtbDIvbG9nb3V0Ii8+CiAgICAgIDxtZDpOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQ8L21kOk5hbWVJREZvcm1hdD4KICAgICAgPG1kOlNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbWF1dGljLWRldi1lZC5teS5zYWxlc2ZvcmNlLmNvbS9pZHAvZW5kcG9pbnQvSHR0cFBvc3QiLz4KICAgICAgPG1kOlNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUmVkaXJlY3QiIExvY2F0aW9uPSJodHRwczovL21hdXRpYy1kZXYtZWQubXkuc2FsZXNmb3JjZS5jb20vaWRwL2VuZHBvaW50L0h0dHBSZWRpcmVjdCIvPgogICA8L21kOklEUFNTT0Rlc2NyaXB0b3I+CjwvbWQ6RW50aXR5RGVzY3JpcHRvcj4='
);
$this->assertFalse($store->has('foobar'));
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Mautic\UserBundle\Tests\Security\SAML\Store;
use Doctrine\Persistence\ObjectManager;
use LightSaml\Provider\TimeProvider\TimeProviderInterface;
use Mautic\UserBundle\Entity\IdEntry;
use Mautic\UserBundle\Security\SAML\Store\IdStore;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class IdStoreTest extends TestCase
{
/**
* @var ObjectManager|MockObject
*/
private MockObject $manager;
/**
* @var TimeProviderInterface|MockObject
*/
private MockObject $timeProvider;
private IdStore $store;
protected function setUp(): void
{
$this->manager = $this->createMock(ObjectManager::class);
$this->timeProvider = $this->createMock(TimeProviderInterface::class);
$this->store = new IdStore($this->manager, $this->timeProvider);
}
public function testNewIdEntryCreatedIfEntityIdNotFound(): void
{
$expiry = new \DateTime('+5 minutes');
$this->manager->expects($this->once())
->method('persist')
->willReturnCallback(function (IdEntry $idEntry) use ($expiry): void {
$this->assertEquals('foobar', $idEntry->getEntityId());
$this->assertEquals('abc', $idEntry->getId());
$this->assertEquals($expiry->getTimestamp(), $idEntry->getExpiryTime()->getTimestamp());
});
$this->store->set('foobar', 'abc', $expiry);
}
public function testIdEntryUpdatedIfEntityIdFound(): void
{
$expiry = new \DateTime('+5 minutes');
$idEntry = new IdEntry();
$idEntry->setEntityId('foobar');
$idEntry->setId('abc');
$idEntry->setExpiryTime($expiry);
$this->manager->expects($this->once())
->method('find')
->willReturn($idEntry);
$this->manager->expects($this->once())
->method('persist')
->with($idEntry);
$this->store->set('foobar', 'abc', $expiry);
}
public function testIdEntryIsFoundAndNotExpired(): void
{
$expiry = new \DateTime('+5 minutes');
$idEntry = new IdEntry();
$idEntry->setEntityId('foobar');
$idEntry->setId('abc');
$idEntry->setExpiryTime($expiry);
$this->manager->expects($this->once())
->method('find')
->willReturn($idEntry);
$this->assertTrue($this->store->has('foobar', 'abc'));
}
public function testIdEntryIsFoundButIsExpired(): void
{
$this->timeProvider->expects($this->once())
->method('getTimestamp')
->willReturn(time());
$expiry = new \DateTime('-5 minutes');
$idEntry = new IdEntry();
$idEntry->setEntityId('foobar');
$idEntry->setId('abc');
$idEntry->setExpiryTime($expiry);
$this->manager->expects($this->once())
->method('find')
->willReturn($idEntry);
$this->assertFalse($this->store->has('foobar', 'abc'));
}
public function testIdEntryIsNotFound(): void
{
$this->timeProvider->expects($this->never())
->method('getTimestamp');
$this->manager->expects($this->once())
->method('find')
->willReturn(null);
$this->assertFalse($this->store->has('foobar', 'abc'));
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security\SAML\Store\Request;
use LightSaml\State\Request\RequestState;
use Mautic\CacheBundle\Cache\CacheProviderInterface;
use Mautic\UserBundle\Security\SAML\Store\Request\RequestStateStore;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\CacheItem;
class RequestStateStoreTest extends TestCase
{
/**
* @var CacheProviderInterface&MockObject
*/
private MockObject $cacheProvider;
private CacheItem $cacheItem;
private RequestStateStore $requestStateStore;
private string $cachePrefix = 'prefix_suffix';
private string $stateId = 'state_id';
protected function setUp(): void
{
parent::setUp();
$this->cacheItem = new CacheItem();
$this->cacheProvider = $this->createMock(CacheProviderInterface::class);
$this->cacheProvider->method('getItem')
->with($this->cachePrefix.$this->stateId)
->willReturn($this->cacheItem);
$this->requestStateStore = new RequestStateStore($this->cacheProvider, 'prefix', '_suffix');
}
public function testSet(): void
{
$state = $this->createMock(RequestState::class);
$state->expects(self::once())
->method('getId')
->willReturn($this->stateId);
$this->cacheProvider->expects(self::once())
->method('save')
->with($this->cacheItem);
$this->requestStateStore->set($state);
$check = \Closure::bind(
static function (CacheItem $actual, TestCase $case) use ($state): void {
$case::assertEqualsWithDelta(
(2 * 60) + microtime(true),
$actual->expiry,
1
);
$case::assertSame($state, $actual->value);
},
null,
CacheItem::class
);
($check)($this->cacheItem, $this);
}
public function testGetNotHit(): void
{
self::assertNull($this->requestStateStore->get($this->stateId));
}
public function testGetIsHitButNotRequestState(): void
{
$setUp = \Closure::bind(
static function (CacheItem $item): void {
$item->isHit = true;
$item->value = 'string';
},
null,
CacheItem::class
);
($setUp)($this->cacheItem);
self::assertNull($this->requestStateStore->get($this->stateId));
}
public function testGetIsHitRequestState(): void
{
$state = $this->createMock(RequestState::class);
$setUp = \Closure::bind(
static function (CacheItem $item, RequestState $state): void {
$item->isHit = true;
$item->value = $state;
},
null,
CacheItem::class
);
($setUp)($this->cacheItem, $state);
self::assertSame($state, $this->requestStateStore->get($this->stateId));
}
public function testRemove(): void
{
$id = 'whatever';
$this->cacheProvider->expects(self::once())
->method('deleteItem')
->with($this->cachePrefix.$id)
->willReturn(true);
self::assertTrue($this->requestStateStore->remove($id));
}
public function testClear(): void
{
$this->cacheProvider->expects(self::once())
->method('clear')
->with($this->cachePrefix);
$this->requestStateStore->clear();
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Mautic\UserBundle\Tests\Security\SAML\Store;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\UserBundle\Security\SAML\Store\TrustOptionsStore;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class TrustOptionsStoreTest extends TestCase
{
/**
* @var CoreParametersHelper|MockObject
*/
private MockObject $coreParametersHelper;
private TrustOptionsStore $store;
protected function setUp(): void
{
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->store = new TrustOptionsStore($this->coreParametersHelper, 'foobar');
}
public function testHasTrustOptionsIfSamlConfiguredAndEntityIdMatches(): void
{
$this->coreParametersHelper->expects($this->once())
->method('get')
->with('saml_idp_metadata')
->willReturn('1');
$this->assertTrue($this->store->has('foobar'));
}
public function testNotHaveTrustOptionsIfSamlDisabled(): void
{
$this->coreParametersHelper->expects($this->once())
->method('get')
->with('saml_idp_metadata')
->willReturn('');
$this->assertFalse($this->store->has('foobar'));
}
public function testNotHaveTrustOptionsIfEntityIdDoesNotMatch(): void
{
$this->coreParametersHelper->expects($this->once())
->method('get')
->with('saml_idp_metadata')
->willReturn('1');
$this->assertFalse($this->store->has('barfoo'));
}
public function testTrustOptionsDoNotSignRequestForDefault(): void
{
$this->coreParametersHelper->expects($this->once())
->method('get')
->with('saml_idp_own_certificate')
->willReturn('');
$store = $this->store->get('foobar');
$this->assertFalse($store->getSignAuthnRequest());
}
public function testTrustOptionsSignRequestForCustom(): void
{
$this->coreParametersHelper->expects($this->once())
->method('get')
->with('saml_idp_own_certificate')
->willReturn('abc');
$store = $this->store->get('foobar');
$this->assertTrue($store->getSignAuthnRequest());
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Mautic\UserBundle\Tests\Security\SAML\User;
use LightSaml\Model\Assertion\Assertion;
use LightSaml\Model\Assertion\Attribute;
use LightSaml\Model\Assertion\AttributeStatement;
use LightSaml\Model\Protocol\Response;
use Mautic\UserBundle\Security\SAML\User\UserMapper;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class UserMapperTest extends TestCase
{
private UserMapper $mapper;
/**
* @var Response|MockObject
*/
private MockObject $response;
protected function setUp(): void
{
$this->mapper = new UserMapper(
[
'email' => 'EmailAddress',
'firstname' => 'FirstName',
'lastname' => 'LastName',
'username' => null,
]
);
$emailAttribute = $this->createMock(Attribute::class);
$emailAttribute->method('getFirstAttributeValue')
->willReturn('hello@there.com');
$firstnameAttribute = $this->createMock(Attribute::class);
$firstnameAttribute->method('getFirstAttributeValue')
->willReturn('Joe');
$lastnameAttribute = $this->createMock(Attribute::class);
$lastnameAttribute->method('getFirstAttributeValue')
->willReturn('Smith');
$defaultAttribute = $this->createMock(Attribute::class);
$defaultAttribute->method('getFirstAttributeValue')
->willReturn('default');
$statement = $this->createMock(AttributeStatement::class);
$statement->method('getFirstAttributeByName')
->willReturnCallback(
fn ($attributeName) => match ($attributeName) {
'EmailAddress' => $emailAttribute,
'FirstName' => $firstnameAttribute,
'LastName' => $lastnameAttribute,
default => $defaultAttribute,
}
);
$assertion = $this->createMock(Assertion::class);
$assertion->method('getAllAttributeStatements')
->willReturn([$statement]);
$this->response = $this->createMock(Response::class);
$this->response->method('getAllAssertions')
->willReturn([$assertion]);
}
public function testUserEntityIsPopulatedFromAssertions(): void
{
$user = $this->mapper->getUser($this->response);
$this->assertEquals('hello@there.com', $user->getEmail());
$this->assertEquals('hello@there.com', $user->getUserIdentifier());
$this->assertEquals('Joe', $user->getFirstName());
$this->assertEquals('Smith', $user->getLastName());
}
public function testUsernameIsReturned(): void
{
$username = $this->mapper->getUsername($this->response);
$this->assertEquals('hello@there.com', $username);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Tests\Traits\CreateEntityTrait;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\Request;
class UserLoginTest extends MauticMysqlTestCase
{
use CreateEntityTrait;
protected function setUp(): void
{
if (strpos($this->name(), 'WithSaml') > 0) {
$this->configParams['saml_idp_metadata'] = 'any_string';
}
parent::setUp();
}
/**
* User can login with acquia email.
*/
public function testSuccessfulLoginWithAcquiaUserWithSaml(): void
{
$this->logoutUser();
$password = Uuid::uuid4()->toString();
$this->createUser($this->createRole(), 'test@acquia.com', $password);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request(Request::METHOD_GET, '/s/login');
// Get the form
$form = $crawler->filter('form')->form();
$form->setValues([
'_username' => 'test@acquia.com',
'_password' => $password,
]);
$crawler = $this->client->submit($form);
$clientResponse = $this->client->getResponse();
$this->assertEquals(200, $clientResponse->getStatusCode());
// user has logged in
$title = $crawler->filterXPath('//head/title')->text();
$this->assertStringContainsString('Dashboard |', $title);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Security;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
use Mautic\UserBundle\Entity\User;
use Mautic\UserBundle\Model\UserModel;
use Mautic\UserBundle\Security\UserTokenSetter;
use PHPUnit\Framework\MockObject\MockObject;
class UserTokenSetterTest extends AbstractMauticTestCase
{
public function testSetUserMakesTheUserAvailableToUserHelper(): void
{
/** @var MockObject&UserModel $userModel */
$userModel = $this->createMock(UserModel::class);
$user = new User();
$userModel->method('getEntity')
->with(1)
->willReturn($user);
$userTokenSetter = new UserTokenSetter($userModel, $this->getContainer()->get('security.token_storage'));
$userTokenSetter->setUser(1);
/** @var UserHelper $userHelper */
$userHelper = $this->getContainer()->get('mautic.helper.user');
$this->assertSame($user, $userHelper->getUser());
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Mautic\UserBundle\Tests\Traits;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
trait CreateEntityTrait
{
public function createRole(bool $isAdmin = false): Role
{
$role = new Role();
$role->setName('Role');
$role->setIsAdmin($isAdmin);
$this->em->persist($role);
return $role;
}
public function createUser(Role $role, string $email = 'test@acquia.com', string $password = 'mautic'): User
{
$userName = explode('@', $email)[0].random_int(1000, 9999);
$user = new User();
$user->setFirstName('John');
$user->setLastName('Doe');
$user->setUsername($userName);
$user->setEmail($email);
$encoder = $this->getContainer()->get(UserPasswordHasherInterface::class);
$user->setPassword($encoder->hashPassword($user, $password));
$user->setRole($role);
$this->em->persist($user);
return $user;
}
}