Initial commit: CloudOps infrastructure platform

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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