|
@@ -0,0 +1,363 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Tests\Unit\Security\Voter;
|
|
|
|
|
+
|
|
|
|
|
+use App\Entity\Access\Access;
|
|
|
|
|
+use App\Entity\Person\Person;
|
|
|
|
|
+use App\Security\Voter\AbstractVoter;
|
|
|
|
|
+use App\Service\Access\Utils;
|
|
|
|
|
+use App\Service\Security\InternalRequestsService;
|
|
|
|
|
+use App\Service\Security\SwitchUser;
|
|
|
|
|
+use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
|
+use PHPUnit\Framework\MockObject\MockObject;
|
|
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
|
|
+use Symfony\Bundle\SecurityBundle\Security;
|
|
|
|
|
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|
|
|
|
+
|
|
|
|
|
+class TestableAbstractVoter extends AbstractVoter {
|
|
|
|
|
+ public const READ = 'READ';
|
|
|
|
|
+ public const EDIT = 'EDIT';
|
|
|
|
|
+ public const CREATE = 'CREATE';
|
|
|
|
|
+ public const DELETE = 'DELETE';
|
|
|
|
|
+
|
|
|
|
|
+ public static function setEntityClass(?string $entityClass): void {
|
|
|
|
|
+ static::$entityClass = $entityClass;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static function getEntityClass(): ?string {
|
|
|
|
|
+ return static::$entityClass;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param array<string> $allowedOperations
|
|
|
|
|
+ * @return void
|
|
|
|
|
+ */
|
|
|
|
|
+ public static function setAllowedOperations(array $allowedOperations): void {
|
|
|
|
|
+ static::$allowedOperations = $allowedOperations;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static function getAllowedOperations(): array {
|
|
|
|
|
+ return static::$allowedOperations;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function supports(string $attribute, mixed $subject): bool {
|
|
|
|
|
+ return parent::supports($attribute, $subject);
|
|
|
|
|
+ }
|
|
|
|
|
+ public function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool {
|
|
|
|
|
+ return parent::voteOnAttribute($attribute, $subject, $token);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function canView(object $subject): bool { return true; }
|
|
|
|
|
+
|
|
|
|
|
+ public function canEdit(object $subject): bool { return true; }
|
|
|
|
|
+
|
|
|
|
|
+ public function canCreate(object $subject): bool { return true; }
|
|
|
|
|
+
|
|
|
|
|
+ public function canDelete(object $subject): bool { return true; }
|
|
|
|
|
+
|
|
|
|
|
+ public function isUserLoggedIn(): bool { return parent::isUserLoggedIn(); }
|
|
|
|
|
+ public function getUser(): ?Access { return parent::getUser(); }
|
|
|
|
|
+ public function isValidInternalRequest(): bool { return parent::isValidInternalRequest(); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class AbstractVoterTest extends TestCase
|
|
|
|
|
+{
|
|
|
|
|
+ protected Security | MockObject $security;
|
|
|
|
|
+ protected Utils | MockObject $accessUtils;
|
|
|
|
|
+ protected EntityManagerInterface | MockObject $entityManager;
|
|
|
|
|
+ protected InternalRequestsService | MockObject $internalRequestsService;
|
|
|
|
|
+ protected SwitchUser | MockObject $switchUserService;
|
|
|
|
|
+
|
|
|
|
|
+ private ?string $initialEntityClass = null;
|
|
|
|
|
+ private ?array $initialAllowedOperations = null;
|
|
|
|
|
+ private ?int $initialSwitchHeader = null;
|
|
|
|
|
+ private ?string $initialRemoteAddr = null;
|
|
|
|
|
+ private ?string $initialInternalRequestToken = null;
|
|
|
|
|
+
|
|
|
|
|
+ public function __construct(?string $name = null, array $data = [], $dataName = '')
|
|
|
|
|
+ {
|
|
|
|
|
+ parent::__construct($name, $data, $dataName);
|
|
|
|
|
+
|
|
|
|
|
+ $this->initialEntityClass = TestableAbstractVoter::getEntityClass();
|
|
|
|
|
+ $this->initialAllowedOperations = TestableAbstractVoter::getAllowedOperations();
|
|
|
|
|
+ $this->initialSwitchHeader = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
|
|
|
|
|
+ $this->initialRemoteAddr = $_SERVER['REMOTE_ADDR'] ?? null;
|
|
|
|
|
+ $this->initialInternalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function reinit() {
|
|
|
|
|
+ // Reinitialize the TestableAbstractVoter static properties
|
|
|
|
|
+ TestableAbstractVoter::setEntityClass($this->initialEntityClass);
|
|
|
|
|
+ TestableAbstractVoter::setAllowedOperations($this->initialAllowedOperations);
|
|
|
|
|
+
|
|
|
|
|
+ // Reinitialize the global variables
|
|
|
|
|
+ if ($this->initialSwitchHeader !== null) {
|
|
|
|
|
+ $_SERVER['HTTP_X_SWITCH_USER'] = $this->initialSwitchHeader;
|
|
|
|
|
+ } else if (array_key_exists('HTTP_X_SWITCH_USER', $_SERVER)) {
|
|
|
|
|
+ unset($_SERVER['HTTP_X_SWITCH_USER']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $_SERVER['REMOTE_ADDR'] = $this->initialRemoteAddr;
|
|
|
|
|
+
|
|
|
|
|
+ if ($this->initialSwitchHeader !== null) {
|
|
|
|
|
+ $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] = $this->initialSwitchHeader;
|
|
|
|
|
+ } else if (array_key_exists('HTTP_INTERNAL_REQUESTS_TOKEN', $_SERVER)) {
|
|
|
|
|
+ unset($_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN']);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function setUp(): void {
|
|
|
|
|
+ $this->security = $this->getMockBuilder(Security::class)->disableOriginalConstructor()->getMock();
|
|
|
|
|
+ $this->accessUtils = $this->getMockBuilder(Utils::class)->disableOriginalConstructor()->getMock();
|
|
|
|
|
+ $this->internalRequestsService = $this->getMockBuilder(InternalRequestsService::class)->disableOriginalConstructor()->getMock();
|
|
|
|
|
+ $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
|
|
|
|
|
+ $this->switchUserService = $this->getMockBuilder(SwitchUser::class)->disableOriginalConstructor()->getMock();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function tearDown(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->reinit();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function makeAbstractVoterMockFor(string $methodName): MockObject | TestableAbstractVoter {
|
|
|
|
|
+ return $this->getMockBuilder(TestableAbstractVoter::class)
|
|
|
|
|
+ ->setConstructorArgs([
|
|
|
|
|
+ $this->security,
|
|
|
|
|
+ $this->accessUtils,
|
|
|
|
|
+ $this->internalRequestsService,
|
|
|
|
|
+ $this->entityManager,
|
|
|
|
|
+ $this->switchUserService
|
|
|
|
|
+ ])
|
|
|
|
|
+ ->setMethodsExcept([$methodName])
|
|
|
|
|
+ ->getMock();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testSupports(): void {
|
|
|
|
|
+ TestableAbstractVoter::setEntityClass(Access::class);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('supports');
|
|
|
|
|
+
|
|
|
|
|
+ $access = new Access();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::READ, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::EDIT, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::CREATE, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::DELETE, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testSupportsNotSupportedClass(): void {
|
|
|
|
|
+ TestableAbstractVoter::setEntityClass(Access::class);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('supports');
|
|
|
|
|
+
|
|
|
|
|
+ $person = new Person();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertFalse(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::READ, $person)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testSupportsNotAllowedOperation(): void {
|
|
|
|
|
+ TestableAbstractVoter::setEntityClass(Access::class);
|
|
|
|
|
+ TestableAbstractVoter::setAllowedOperations([TestableAbstractVoter::READ]);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('supports');
|
|
|
|
|
+
|
|
|
|
|
+ $access = new Access();
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::READ, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertFalse(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::EDIT, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertFalse(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::CREATE, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ $this->assertFalse(
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::DELETE, $access)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testSupportsMissingEntityClass(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('supports');
|
|
|
|
|
+
|
|
|
|
|
+ $this->expectException(\RuntimeException::class);
|
|
|
|
|
+ $this->expectExceptionMessage('Setup the self::$entityClass property, or override the supports() method');
|
|
|
|
|
+
|
|
|
|
|
+ $access = new Access();
|
|
|
|
|
+ $abstractVoter->supports(TestableAbstractVoter::READ, $access);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testVoteOnAttributeRead(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
|
|
|
|
|
+
|
|
|
|
|
+ $subject = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+ $token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::once())->method('canView')->with($subject)->willReturn(true);
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canEdit');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canCreate');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canDelete');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->voteOnAttribute(TestableAbstractVoter::READ, $subject, $token)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testVoteOnAttributeEdit(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
|
|
|
|
|
+
|
|
|
|
|
+ $subject = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+ $token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canView');
|
|
|
|
|
+ $abstractVoter->expects(self::once())->method('canEdit')->with($subject)->willReturn(true);
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canCreate');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canDelete');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->voteOnAttribute(TestableAbstractVoter::EDIT, $subject, $token)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testVoteOnAttributeCreate(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
|
|
|
|
|
+
|
|
|
|
|
+ $subject = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+ $token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canView');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canEdit');
|
|
|
|
|
+ $abstractVoter->expects(self::once())->method('canCreate')->with($subject)->willReturn(true);
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canDelete');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->voteOnAttribute(TestableAbstractVoter::CREATE, $subject, $token)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testVoteOnAttributeDelete(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
|
|
|
|
|
+
|
|
|
|
|
+ $subject = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+ $token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canView');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canEdit');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canCreate');
|
|
|
|
|
+ $abstractVoter->expects(self::once())->method('canDelete')->with($subject)->willReturn(true);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue(
|
|
|
|
|
+ $abstractVoter->voteOnAttribute(TestableAbstractVoter::DELETE, $subject, $token)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testVoteOnAttributeInvalidAttribute(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('voteOnAttribute');
|
|
|
|
|
+
|
|
|
|
|
+ $subject = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+ $token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canView');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canEdit');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canCreate');
|
|
|
|
|
+ $abstractVoter->expects(self::never())->method('canDelete');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertFalse(
|
|
|
|
|
+ $abstractVoter->voteOnAttribute('other_attr', $subject, $token)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testGetUser() {
|
|
|
|
|
+
|
|
|
|
|
+ $user = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $this->security->expects(self::once())->method('getUser')->willReturn($user);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('getUser');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertEquals(
|
|
|
|
|
+ $user,
|
|
|
|
|
+ $abstractVoter->getUser()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testGetUserNoUser() {
|
|
|
|
|
+
|
|
|
|
|
+ $this->security->expects(self::once())->method('getUser')->willReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('getUser');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertNull(
|
|
|
|
|
+ $abstractVoter->getUser()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testGetUserSwitchUser() {
|
|
|
|
|
+
|
|
|
|
|
+ $originalUser = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+ $switchUser = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $_SERVER['HTTP_X_SWITCH_USER'] = 1;
|
|
|
|
|
+
|
|
|
|
|
+ $this->security->expects(self::once())->method('getUser')->willReturn($originalUser);
|
|
|
|
|
+ $this->entityManager->expects(self::once())->method('find')->with(Access::class, 1)->willReturn($switchUser);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('getUser');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertEquals(
|
|
|
|
|
+ $switchUser,
|
|
|
|
|
+ $abstractVoter->getUser()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testIsUserLoggedIn(): void {
|
|
|
|
|
+ $user = $this->getMockBuilder(Access::class)->getMock();
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('isUserLoggedIn');
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::once())->method('getUser')->willReturn($user);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue($abstractVoter->isUserLoggedIn());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testIsUserLoggedInNoUser(): void {
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('isUserLoggedIn');
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter->expects(self::once())->method('getUser')->willReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertFalse($abstractVoter->isUserLoggedIn());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testIsValidInternalRequest(): void {
|
|
|
|
|
+
|
|
|
|
|
+ $clientIp = '0.0.0.0';
|
|
|
|
|
+ $token = 'valid_token';
|
|
|
|
|
+
|
|
|
|
|
+ $_SERVER['REMOTE_ADDR'] = $clientIp;
|
|
|
|
|
+ $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] = $token;
|
|
|
|
|
+
|
|
|
|
|
+ $this->internalRequestsService
|
|
|
|
|
+ ->expects(self::once())
|
|
|
|
|
+ ->method('isAllowed')
|
|
|
|
|
+ ->with($clientIp, $token)
|
|
|
|
|
+ ->willReturn(true);
|
|
|
|
|
+
|
|
|
|
|
+ $abstractVoter = $this->makeAbstractVoterMockFor('isValidInternalRequest');
|
|
|
|
|
+
|
|
|
|
|
+ $this->assertTrue($abstractVoter->isValidInternalRequest());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+}
|