Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user