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