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

View File

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

View File

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

View File

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

View File

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

View File

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