diff options
Diffstat (limited to 'tests/lib/Security')
32 files changed, 1553 insertions, 1010 deletions
diff --git a/tests/lib/Security/Bruteforce/Backend/MemoryCacheBackendTest.php b/tests/lib/Security/Bruteforce/Backend/MemoryCacheBackendTest.php new file mode 100644 index 00000000000..e0289fa7ca9 --- /dev/null +++ b/tests/lib/Security/Bruteforce/Backend/MemoryCacheBackendTest.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Security\Bruteforce\Backend; + +use OC\Security\Bruteforce\Backend\IBackend; +use OC\Security\Bruteforce\Backend\MemoryCacheBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\ICache; +use OCP\ICacheFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class MemoryCacheBackendTest extends TestCase { + /** @var ICacheFactory|MockObject */ + private $cacheFactory; + /** @var ITimeFactory|MockObject */ + private $timeFactory; + /** @var ICache|MockObject */ + private $cache; + private IBackend $backend; + + protected function setUp(): void { + parent::setUp(); + + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->cache = $this->createMock(ICache::class); + + $this->cacheFactory + ->expects($this->once()) + ->method('createDistributed') + ->with(MemoryCacheBackend::class) + ->willReturn($this->cache); + + $this->backend = new MemoryCacheBackend( + $this->cacheFactory, + $this->timeFactory + ); + } + + public function testGetAttemptsWithNoAttemptsBefore(): void { + $this->cache + ->expects($this->once()) + ->method('get') + ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') + ->willReturn(null); + + $this->assertSame(0, $this->backend->getAttempts('10.10.10.10/32', 0)); + } + + public static function dataGetAttempts(): array { + return [ + [0, null, null, 4], + [100, null, null, 2], + [0, 'action1', null, 2], + [100, 'action1', null, 1], + [0, 'action1', ['metadata2'], 1], + [100, 'action1', ['metadata2'], 1], + [100, 'action1', ['metadata1'], 0], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetAttempts')] + public function testGetAttempts(int $maxAge, ?string $action, ?array $metadata, int $expected): void { + $this->cache + ->expects($this->once()) + ->method('get') + ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') + ->willReturn(json_encode([ + '1' . '#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata1'])), + '300' . '#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata2'])), + '1' . '#' . hash('sha1', 'action2') . '#' . hash('sha1', json_encode(['metadata1'])), + '300' . '#' . hash('sha1', 'action2') . '#' . hash('sha1', json_encode(['metadata2'])), + ])); + + $this->assertSame($expected, $this->backend->getAttempts('10.10.10.10/32', $maxAge, $action, $metadata)); + } + + public function testRegisterAttemptWithNoAttemptsBefore(): void { + $this->cache + ->expects($this->once()) + ->method('get') + ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') + ->willReturn(null); + $this->cache + ->expects($this->once()) + ->method('set') + ->with( + '8b9da631d1f7b022bb2c3c489e16092f82b42fd4', + json_encode(['223#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata1']))]) + ); + + $this->backend->registerAttempt('10.10.10.10', '10.10.10.10/32', 223, 'action1', ['metadata1']); + } + + public function testRegisterAttempt(): void { + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(12 * 3600 + 86); + + $this->cache + ->expects($this->once()) + ->method('get') + ->with('8b9da631d1f7b022bb2c3c489e16092f82b42fd4') + ->willReturn(json_encode([ + '1#' . hash('sha1', 'action1') . '#' . hash('sha1', json_encode(['metadata1'])), + '2#' . hash('sha1', 'action2') . '#' . hash('sha1', json_encode(['metadata1'])), + '87#' . hash('sha1', 'action3') . '#' . hash('sha1', json_encode(['metadata1'])), + '123#' . hash('sha1', 'action4') . '#' . hash('sha1', json_encode(['metadata1'])), + '123#' . hash('sha1', 'action5') . '#' . hash('sha1', json_encode(['metadata1'])), + '124#' . hash('sha1', 'action6') . '#' . hash('sha1', json_encode(['metadata1'])), + ])); + $this->cache + ->expects($this->once()) + ->method('set') + ->with( + '8b9da631d1f7b022bb2c3c489e16092f82b42fd4', + json_encode([ + '87#' . hash('sha1', 'action3') . '#' . hash('sha1', json_encode(['metadata1'])), + '123#' . hash('sha1', 'action4') . '#' . hash('sha1', json_encode(['metadata1'])), + '123#' . hash('sha1', 'action5') . '#' . hash('sha1', json_encode(['metadata1'])), + '124#' . hash('sha1', 'action6') . '#' . hash('sha1', json_encode(['metadata1'])), + '186#' . hash('sha1', 'action7') . '#' . hash('sha1', json_encode(['metadata2'])), + ]) + ); + + $this->backend->registerAttempt('10.10.10.10', '10.10.10.10/32', 186, 'action7', ['metadata2']); + } +} diff --git a/tests/lib/Security/Bruteforce/CapabilitiesTest.php b/tests/lib/Security/Bruteforce/CapabilitiesTest.php index cd43d94f8cb..438a24f2240 100644 --- a/tests/lib/Security/Bruteforce/CapabilitiesTest.php +++ b/tests/lib/Security/Bruteforce/CapabilitiesTest.php @@ -1,29 +1,17 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2017 Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\Bruteforce; use OC\Security\Bruteforce\Capabilities; -use OC\Security\Bruteforce\Throttler; use OCP\IRequest; +use OCP\Security\Bruteforce\IThrottler; use Test\TestCase; class CapabilitiesTest extends TestCase { @@ -33,7 +21,7 @@ class CapabilitiesTest extends TestCase { /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ private $request; - /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */ + /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ private $throttler; protected function setUp(): void { @@ -41,7 +29,7 @@ class CapabilitiesTest extends TestCase { $this->request = $this->createMock(IRequest::class); - $this->throttler = $this->createMock(Throttler::class); + $this->throttler = $this->createMock(IThrottler::class); $this->capabilities = new Capabilities( $this->request, @@ -49,18 +37,24 @@ class CapabilitiesTest extends TestCase { ); } - public function testGetCapabilities() { + public function testGetCapabilities(): void { $this->throttler->expects($this->atLeastOnce()) ->method('getDelay') ->with('10.10.10.10') ->willReturn(42); + $this->throttler->expects($this->atLeastOnce()) + ->method('isBypassListed') + ->with('10.10.10.10') + ->willReturn(true); + $this->request->method('getRemoteAddress') ->willReturn('10.10.10.10'); $expected = [ 'bruteforce' => [ - 'delay' => 42 + 'delay' => 42, + 'allow-listed' => true, ] ]; $result = $this->capabilities->getCapabilities(); @@ -68,7 +62,7 @@ class CapabilitiesTest extends TestCase { $this->assertEquals($expected, $result); } - public function testGetCapabilitiesOnCli() { + public function testGetCapabilitiesOnCli(): void { $this->throttler->expects($this->atLeastOnce()) ->method('getDelay') ->with('') @@ -79,7 +73,8 @@ class CapabilitiesTest extends TestCase { $expected = [ 'bruteforce' => [ - 'delay' => 0 + 'delay' => 0, + 'allow-listed' => false, ] ]; $result = $this->capabilities->getCapabilities(); diff --git a/tests/lib/Security/Bruteforce/ThrottlerTest.php b/tests/lib/Security/Bruteforce/ThrottlerTest.php deleted file mode 100644 index b4a7016bc20..00000000000 --- a/tests/lib/Security/Bruteforce/ThrottlerTest.php +++ /dev/null @@ -1,239 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -namespace Test\Security\Bruteforce; - -use OC\AppFramework\Utility\TimeFactory; -use OC\Security\Bruteforce\Throttler; -use OCP\IConfig; -use OCP\IDBConnection; -use OCP\ILogger; -use Test\TestCase; - -/** - * Based on the unit tests from Paragonie's Airship CMS - * Ref: https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/test/unit/Engine/Security/AirBrakeTest.php - * - * @package Test\Security\Bruteforce - */ -class ThrottlerTest extends TestCase { - /** @var Throttler */ - private $throttler; - /** @var IDBConnection */ - private $dbConnection; - /** @var ILogger */ - private $logger; - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ - private $config; - - protected function setUp(): void { - $this->dbConnection = $this->createMock(IDBConnection::class); - $this->logger = $this->createMock(ILogger::class); - $this->config = $this->createMock(IConfig::class); - - $this->throttler = new Throttler( - $this->dbConnection, - new TimeFactory(), - $this->logger, - $this->config - ); - parent::setUp(); - } - - public function testCutoff() { - // precisely 31 second shy of 12 hours - $cutoff = self::invokePrivate($this->throttler, 'getCutoff', [43169]); - $this->assertSame(0, $cutoff->y); - $this->assertSame(0, $cutoff->m); - $this->assertSame(0, $cutoff->d); - $this->assertSame(11, $cutoff->h); - $this->assertSame(59, $cutoff->i); - $this->assertSame(29, $cutoff->s); - $cutoff = self::invokePrivate($this->throttler, 'getCutoff', [86401]); - $this->assertSame(0, $cutoff->y); - $this->assertSame(0, $cutoff->m); - $this->assertSame(1, $cutoff->d); - $this->assertSame(0, $cutoff->h); - $this->assertSame(0, $cutoff->i); - // Leap second tolerance: - $this->assertLessThan(2, $cutoff->s); - } - - public function dataIsIPWhitelisted() { - return [ - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.0/24', - ], - true, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '192.168.0.0/16', - ], - false, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - ], - true, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.11/31', - ], - true, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.9/31', - ], - false, - ], - [ - '10.10.10.10', - [ - 'whitelist_0' => '10.10.10.15/29', - ], - true, - ], - [ - 'dead:beef:cafe::1', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - 'whitelist_2' => 'deaf:beef:cafe:1234::/64' - ], - false, - ], - [ - 'dead:beef:cafe::1', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - 'whitelist_2' => 'deaf:beef::/64' - ], - false, - ], - [ - 'dead:beef:cafe::1', - [ - 'whitelist_0' => '192.168.0.0/16', - 'whitelist_1' => '10.10.10.0/24', - 'whitelist_2' => 'deaf:cafe::/8' - ], - true, - ], - [ - 'dead:beef:cafe::1111', - [ - 'whitelist_0' => 'dead:beef:cafe::1100/123', - - ], - true, - ], - [ - 'invalid', - [], - false, - ], - ]; - } - - /** - * @param string $ip - * @param string[] $whitelists - * @param bool $isWhiteListed - * @param bool $enabled - */ - private function isIpWhiteListedHelper($ip, - $whitelists, - $isWhiteListed, - $enabled) { - $this->config->method('getAppKeys') - ->with($this->equalTo('bruteForce')) - ->willReturn(array_keys($whitelists)); - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('auth.bruteforce.protection.enabled', true) - ->willReturn($enabled); - - $this->config->method('getAppValue') - ->willReturnCallback(function ($app, $key, $default) use ($whitelists) { - if ($app !== 'bruteForce') { - return $default; - } - if (isset($whitelists[$key])) { - return $whitelists[$key]; - } - return $default; - }); - - $this->assertSame( - ($enabled === false) ? true : $isWhiteListed, - self::invokePrivate($this->throttler, 'isIPWhitelisted', [$ip]) - ); - } - - /** - * @dataProvider dataIsIPWhitelisted - * - * @param string $ip - * @param string[] $whitelists - * @param bool $isWhiteListed - */ - public function testIsIpWhiteListedWithEnabledProtection($ip, - $whitelists, - $isWhiteListed) { - $this->isIpWhiteListedHelper( - $ip, - $whitelists, - $isWhiteListed, - true - ); - } - - /** - * @dataProvider dataIsIPWhitelisted - * - * @param string $ip - * @param string[] $whitelists - * @param bool $isWhiteListed - */ - public function testIsIpWhiteListedWithDisabledProtection($ip, - $whitelists, - $isWhiteListed) { - $this->isIpWhiteListedHelper( - $ip, - $whitelists, - $isWhiteListed, - false - ); - } -} diff --git a/tests/lib/Security/CSP/AddContentSecurityPolicyEventTest.php b/tests/lib/Security/CSP/AddContentSecurityPolicyEventTest.php index ef894bb56f3..39dd7a95890 100644 --- a/tests/lib/Security/CSP/AddContentSecurityPolicyEventTest.php +++ b/tests/lib/Security/CSP/AddContentSecurityPolicyEventTest.php @@ -1,26 +1,10 @@ <?php declare(strict_types=1); + /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\CSP; @@ -31,7 +15,7 @@ use OCP\Security\CSP\AddContentSecurityPolicyEvent; use Test\TestCase; class AddContentSecurityPolicyEventTest extends TestCase { - public function testAddEvent() { + public function testAddEvent(): void { $cspManager = $this->createMock(ContentSecurityPolicyManager::class); $policy = $this->createMock(ContentSecurityPolicy::class); $event = new AddContentSecurityPolicyEvent($cspManager); diff --git a/tests/lib/Security/CSP/ContentSecurityPolicyManagerTest.php b/tests/lib/Security/CSP/ContentSecurityPolicyManagerTest.php index 2fc7a53e78d..a32a4132287 100644 --- a/tests/lib/Security/CSP/ContentSecurityPolicyManagerTest.php +++ b/tests/lib/Security/CSP/ContentSecurityPolicyManagerTest.php @@ -1,34 +1,25 @@ <?php + +declare(strict_types=1); + /** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security\CSP; use OC\Security\CSP\ContentSecurityPolicyManager; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\EmptyContentSecurityPolicy; use OCP\EventDispatcher\IEventDispatcher; use OCP\Security\CSP\AddContentSecurityPolicyEvent; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use OCP\Server; use Test\TestCase; class ContentSecurityPolicyManagerTest extends TestCase { - /** @var EventDispatcherInterface */ + /** @var IEventDispatcher */ private $dispatcher; /** @var ContentSecurityPolicyManager */ @@ -36,34 +27,32 @@ class ContentSecurityPolicyManagerTest extends TestCase { protected function setUp(): void { parent::setUp(); - $this->dispatcher = \OC::$server->query(IEventDispatcher::class); + $this->dispatcher = Server::get(IEventDispatcher::class); $this->contentSecurityPolicyManager = new ContentSecurityPolicyManager($this->dispatcher); } - public function testAddDefaultPolicy() { - $this->contentSecurityPolicyManager->addDefaultPolicy(new \OCP\AppFramework\Http\ContentSecurityPolicy()); + public function testAddDefaultPolicy(): void { + $this->contentSecurityPolicyManager->addDefaultPolicy(new ContentSecurityPolicy()); $this->addToAssertionCount(1); } - public function testGetDefaultPolicyWithPolicies() { - $policy = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + public function testGetDefaultPolicyWithPolicies(): void { + $policy = new ContentSecurityPolicy(); $policy->addAllowedFontDomain('mydomain.com'); $policy->addAllowedImageDomain('anotherdomain.de'); $this->contentSecurityPolicyManager->addDefaultPolicy($policy); - $policy = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + $policy = new ContentSecurityPolicy(); $policy->addAllowedFontDomain('example.com'); $policy->addAllowedImageDomain('example.org'); - $policy->allowInlineScript(true); $policy->allowEvalScript(true); $this->contentSecurityPolicyManager->addDefaultPolicy($policy); - $policy = new \OCP\AppFramework\Http\EmptyContentSecurityPolicy(); + $policy = new EmptyContentSecurityPolicy(); $policy->addAllowedChildSrcDomain('childdomain'); $policy->addAllowedFontDomain('anotherFontDomain'); $policy->addAllowedFormActionDomain('thirdDomain'); $this->contentSecurityPolicyManager->addDefaultPolicy($policy); $expected = new \OC\Security\CSP\ContentSecurityPolicy(); - $expected->allowInlineScript(true); $expected->allowEvalScript(true); $expected->addAllowedFontDomain('mydomain.com'); $expected->addAllowedFontDomain('example.com'); @@ -72,32 +61,33 @@ class ContentSecurityPolicyManagerTest extends TestCase { $expected->addAllowedImageDomain('anotherdomain.de'); $expected->addAllowedImageDomain('example.org'); $expected->addAllowedChildSrcDomain('childdomain'); - $expectedStringPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: anotherdomain.de example.org;font-src 'self' data: mydomain.com example.com anotherFontDomain;connect-src 'self';media-src 'self';child-src childdomain;frame-ancestors 'self';form-action 'self' thirdDomain"; + $expectedStringPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: anotherdomain.de example.org;font-src 'self' data: mydomain.com example.com anotherFontDomain;connect-src 'self';media-src 'self';child-src childdomain;frame-ancestors 'self';form-action 'self' thirdDomain"; $this->assertEquals($expected, $this->contentSecurityPolicyManager->getDefaultPolicy()); $this->assertSame($expectedStringPolicy, $this->contentSecurityPolicyManager->getDefaultPolicy()->buildPolicy()); } - public function testGetDefaultPolicyWithPoliciesViaEvent() { - $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e) { - $policy = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + public function testGetDefaultPolicyWithPoliciesViaEvent(): void { + $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e): void { + $policy = new ContentSecurityPolicy(); $policy->addAllowedFontDomain('mydomain.com'); $policy->addAllowedImageDomain('anotherdomain.de'); + $policy->useStrictDynamic(true); + $policy->allowEvalScript(true); $e->addPolicy($policy); }); - $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e) { - $policy = new \OCP\AppFramework\Http\ContentSecurityPolicy(); + $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e): void { + $policy = new ContentSecurityPolicy(); $policy->addAllowedFontDomain('example.com'); $policy->addAllowedImageDomain('example.org'); - $policy->allowInlineScript(true); - $policy->allowEvalScript(true); + $policy->allowEvalScript(false); $e->addPolicy($policy); }); - $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e) { - $policy = new \OCP\AppFramework\Http\EmptyContentSecurityPolicy(); + $this->dispatcher->addListener(AddContentSecurityPolicyEvent::class, function (AddContentSecurityPolicyEvent $e): void { + $policy = new EmptyContentSecurityPolicy(); $policy->addAllowedChildSrcDomain('childdomain'); $policy->addAllowedFontDomain('anotherFontDomain'); $policy->addAllowedFormActionDomain('thirdDomain'); @@ -105,7 +95,6 @@ class ContentSecurityPolicyManagerTest extends TestCase { }); $expected = new \OC\Security\CSP\ContentSecurityPolicy(); - $expected->allowInlineScript(true); $expected->allowEvalScript(true); $expected->addAllowedFontDomain('mydomain.com'); $expected->addAllowedFontDomain('example.com'); @@ -114,7 +103,8 @@ class ContentSecurityPolicyManagerTest extends TestCase { $expected->addAllowedImageDomain('example.org'); $expected->addAllowedChildSrcDomain('childdomain'); $expected->addAllowedFormActionDomain('thirdDomain'); - $expectedStringPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: anotherdomain.de example.org;font-src 'self' data: mydomain.com example.com anotherFontDomain;connect-src 'self';media-src 'self';child-src childdomain;frame-ancestors 'self';form-action 'self' thirdDomain"; + $expected->useStrictDynamic(true); + $expectedStringPolicy = "default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data: blob: anotherdomain.de example.org;font-src 'self' data: mydomain.com example.com anotherFontDomain;connect-src 'self';media-src 'self';child-src childdomain;frame-ancestors 'self';form-action 'self' thirdDomain"; $this->assertEquals($expected, $this->contentSecurityPolicyManager->getDefaultPolicy()); $this->assertSame($expectedStringPolicy, $this->contentSecurityPolicyManager->getDefaultPolicy()->buildPolicy()); diff --git a/tests/lib/Security/CSP/ContentSecurityPolicyNonceManagerTest.php b/tests/lib/Security/CSP/ContentSecurityPolicyNonceManagerTest.php index 783eb35eef9..3765311155a 100644 --- a/tests/lib/Security/CSP/ContentSecurityPolicyNonceManagerTest.php +++ b/tests/lib/Security/CSP/ContentSecurityPolicyNonceManagerTest.php @@ -1,22 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\CSP; @@ -25,42 +13,46 @@ use OC\AppFramework\Http\Request; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OC\Security\CSRF\CsrfToken; use OC\Security\CSRF\CsrfTokenManager; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class ContentSecurityPolicyNonceManagerTest extends TestCase { - /** @var CsrfTokenManager */ - private $csrfTokenManager; - /** @var Request */ + /** @var CsrfTokenManager&MockObject */ + private $CSRFTokenManager; + /** @var Request&MockObject */ private $request; /** @var ContentSecurityPolicyNonceManager */ private $nonceManager; protected function setUp(): void { - $this->csrfTokenManager = $this->createMock(CsrfTokenManager::class); + $this->CSRFTokenManager = $this->createMock(CsrfTokenManager::class); $this->request = $this->createMock(Request::class); $this->nonceManager = new ContentSecurityPolicyNonceManager( - $this->csrfTokenManager, + $this->CSRFTokenManager, $this->request ); } - public function testGetNonce() { + public function testGetNonce(): void { + $secret = base64_encode('secret'); + $tokenValue = base64_encode('secret' ^ 'value_') . ':' . $secret; $token = $this->createMock(CsrfToken::class); $token ->expects($this->once()) ->method('getEncryptedValue') - ->willReturn('MyToken'); + ->willReturn($tokenValue); - $this->csrfTokenManager + $this->CSRFTokenManager ->expects($this->once()) ->method('getToken') ->willReturn($token); - $this->assertSame('TXlUb2tlbg==', $this->nonceManager->getNonce()); - $this->assertSame('TXlUb2tlbg==', $this->nonceManager->getNonce()); + $this->assertSame($secret, $this->nonceManager->getNonce()); + // call it twice but `getEncryptedValue` is expected to be called only once + $this->assertSame($secret, $this->nonceManager->getNonce()); } - public function testGetNonceServerVar() { + public function testGetNonceServerVar(): void { $token = 'SERVERNONCE'; $this->request ->method('__isset') @@ -73,6 +65,7 @@ class ContentSecurityPolicyNonceManagerTest extends TestCase { ->willReturn(['CSP_NONCE' => $token]); $this->assertSame($token, $this->nonceManager->getNonce()); + // call it twice but `CSP_NONCE` variable is expected to be loaded only once $this->assertSame($token, $this->nonceManager->getNonce()); } } diff --git a/tests/lib/Security/CSRF/CsrfTokenGeneratorTest.php b/tests/lib/Security/CSRF/CsrfTokenGeneratorTest.php index 10ab0f00c94..86f458d8ea8 100644 --- a/tests/lib/Security/CSRF/CsrfTokenGeneratorTest.php +++ b/tests/lib/Security/CSRF/CsrfTokenGeneratorTest.php @@ -1,28 +1,20 @@ <?php + +declare(strict_types=1); + /** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security\CSRF; +use OC\Security\CSRF\CsrfTokenGenerator; +use OCP\Security\ISecureRandom; + class CsrfTokenGeneratorTest extends \Test\TestCase { - /** @var \OCP\Security\ISecureRandom */ + /** @var ISecureRandom */ private $random; /** @var \OC\Security\CSRF\CsrfTokenGenerator */ private $csrfTokenGenerator; @@ -31,10 +23,10 @@ class CsrfTokenGeneratorTest extends \Test\TestCase { parent::setUp(); $this->random = $this->getMockBuilder('\OCP\Security\ISecureRandom') ->disableOriginalConstructor()->getMock(); - $this->csrfTokenGenerator = new \OC\Security\CSRF\CsrfTokenGenerator($this->random); + $this->csrfTokenGenerator = new CsrfTokenGenerator($this->random); } - public function testGenerateTokenWithCustomNumber() { + public function testGenerateTokenWithCustomNumber(): void { $this->random ->expects($this->once()) ->method('generate') @@ -43,7 +35,7 @@ class CsrfTokenGeneratorTest extends \Test\TestCase { $this->assertSame('abc', $this->csrfTokenGenerator->generateToken(3)); } - public function testGenerateTokenWithDefault() { + public function testGenerateTokenWithDefault(): void { $this->random ->expects($this->once()) ->method('generate') diff --git a/tests/lib/Security/CSRF/CsrfTokenManagerTest.php b/tests/lib/Security/CSRF/CsrfTokenManagerTest.php index 29fbbfe3b26..66ee18475a4 100644 --- a/tests/lib/Security/CSRF/CsrfTokenManagerTest.php +++ b/tests/lib/Security/CSRF/CsrfTokenManagerTest.php @@ -1,26 +1,18 @@ <?php + +declare(strict_types=1); + /** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security\CSRF; +use OC\Security\CSRF\CsrfToken; +use OC\Security\CSRF\CsrfTokenManager; + class CsrfTokenManagerTest extends \Test\TestCase { /** @var \OC\Security\CSRF\CsrfTokenManager */ private $csrfTokenManager; @@ -36,13 +28,13 @@ class CsrfTokenManagerTest extends \Test\TestCase { $this->storageInterface = $this->getMockBuilder('\OC\Security\CSRF\TokenStorage\SessionStorage') ->disableOriginalConstructor()->getMock(); - $this->csrfTokenManager = new \OC\Security\CSRF\CsrfTokenManager( + $this->csrfTokenManager = new CsrfTokenManager( $this->tokenGenerator, $this->storageInterface ); } - public function testGetTokenWithExistingToken() { + public function testGetTokenWithExistingToken(): void { $this->storageInterface ->expects($this->once()) ->method('hasToken') @@ -52,11 +44,11 @@ class CsrfTokenManagerTest extends \Test\TestCase { ->method('getToken') ->willReturn('MyExistingToken'); - $expected = new \OC\Security\CSRF\CsrfToken('MyExistingToken'); + $expected = new CsrfToken('MyExistingToken'); $this->assertEquals($expected, $this->csrfTokenManager->getToken()); } - public function testGetTokenWithExistingTokenKeepsOnSecondRequest() { + public function testGetTokenWithExistingTokenKeepsOnSecondRequest(): void { $this->storageInterface ->expects($this->once()) ->method('hasToken') @@ -66,13 +58,13 @@ class CsrfTokenManagerTest extends \Test\TestCase { ->method('getToken') ->willReturn('MyExistingToken'); - $expected = new \OC\Security\CSRF\CsrfToken('MyExistingToken'); + $expected = new CsrfToken('MyExistingToken'); $token = $this->csrfTokenManager->getToken(); $this->assertSame($token, $this->csrfTokenManager->getToken()); $this->assertSame($token, $this->csrfTokenManager->getToken()); } - public function testGetTokenWithoutExistingToken() { + public function testGetTokenWithoutExistingToken(): void { $this->storageInterface ->expects($this->once()) ->method('hasToken') @@ -86,11 +78,11 @@ class CsrfTokenManagerTest extends \Test\TestCase { ->method('setToken') ->with('MyNewToken'); - $expected = new \OC\Security\CSRF\CsrfToken('MyNewToken'); + $expected = new CsrfToken('MyNewToken'); $this->assertEquals($expected, $this->csrfTokenManager->getToken()); } - public function testRefreshToken() { + public function testRefreshToken(): void { $this->tokenGenerator ->expects($this->once()) ->method('generateToken') @@ -100,11 +92,11 @@ class CsrfTokenManagerTest extends \Test\TestCase { ->method('setToken') ->with('MyNewToken'); - $expected = new \OC\Security\CSRF\CsrfToken('MyNewToken'); + $expected = new CsrfToken('MyNewToken'); $this->assertEquals($expected, $this->csrfTokenManager->refreshToken()); } - public function testRemoveToken() { + public function testRemoveToken(): void { $this->storageInterface ->expects($this->once()) ->method('removeToken'); @@ -112,22 +104,22 @@ class CsrfTokenManagerTest extends \Test\TestCase { $this->csrfTokenManager->removeToken(); } - public function testIsTokenValidWithoutToken() { + public function testIsTokenValidWithoutToken(): void { $this->storageInterface ->expects($this->once()) ->method('hasToken') ->willReturn(false); - $token = new \OC\Security\CSRF\CsrfToken('Token'); + $token = new CsrfToken('Token'); $this->assertSame(false, $this->csrfTokenManager->isTokenValid($token)); } - public function testIsTokenValidWithWrongToken() { + public function testIsTokenValidWithWrongToken(): void { $this->storageInterface ->expects($this->once()) ->method('hasToken') ->willReturn(true); - $token = new \OC\Security\CSRF\CsrfToken('Token'); + $token = new CsrfToken('Token'); $this->storageInterface ->expects($this->once()) ->method('getToken') @@ -136,20 +128,20 @@ class CsrfTokenManagerTest extends \Test\TestCase { $this->assertSame(false, $this->csrfTokenManager->isTokenValid($token)); } - public function testIsTokenValidWithValidToken() { + public function testIsTokenValidWithValidToken(): void { $a = 'abc'; $b = 'def'; $xorB64 = 'BQcF'; $tokenVal = sprintf('%s:%s', $xorB64, base64_encode($a)); $this->storageInterface - ->expects($this->once()) - ->method('hasToken') - ->willReturn(true); - $token = new \OC\Security\CSRF\CsrfToken($tokenVal); + ->expects($this->once()) + ->method('hasToken') + ->willReturn(true); + $token = new CsrfToken($tokenVal); $this->storageInterface - ->expects($this->once()) - ->method('getToken') - ->willReturn($b); + ->expects($this->once()) + ->method('getToken') + ->willReturn($b); $this->assertSame(true, $this->csrfTokenManager->isTokenValid($token)); } diff --git a/tests/lib/Security/CSRF/CsrfTokenTest.php b/tests/lib/Security/CSRF/CsrfTokenTest.php index fbb92cd315a..5b5ba5ae54f 100644 --- a/tests/lib/Security/CSRF/CsrfTokenTest.php +++ b/tests/lib/Security/CSRF/CsrfTokenTest.php @@ -1,46 +1,37 @@ <?php + +declare(strict_types=1); + /** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security\CSRF; +use OC\Security\CSRF\CsrfToken; + class CsrfTokenTest extends \Test\TestCase { - public function testGetEncryptedValue() { - $csrfToken = new \OC\Security\CSRF\CsrfToken('MyCsrfToken'); + public function testGetEncryptedValue(): void { + $csrfToken = new CsrfToken('MyCsrfToken'); $this->assertSame(33, strlen($csrfToken->getEncryptedValue())); $this->assertSame(':', $csrfToken->getEncryptedValue()[16]); } - public function testGetEncryptedValueStaysSameOnSecondRequest() { - $csrfToken = new \OC\Security\CSRF\CsrfToken('MyCsrfToken'); + public function testGetEncryptedValueStaysSameOnSecondRequest(): void { + $csrfToken = new CsrfToken('MyCsrfToken'); $tokenValue = $csrfToken->getEncryptedValue(); $this->assertSame($tokenValue, $csrfToken->getEncryptedValue()); $this->assertSame($tokenValue, $csrfToken->getEncryptedValue()); } - public function testGetDecryptedValue() { + public function testGetDecryptedValue(): void { $a = 'abc'; $b = 'def'; $xorB64 = 'BQcF'; $tokenVal = sprintf('%s:%s', $xorB64, base64_encode($a)); - $csrfToken = new \OC\Security\CSRF\CsrfToken($tokenVal); + $csrfToken = new CsrfToken($tokenVal); $this->assertSame($b, $csrfToken->getDecryptedValue()); } } diff --git a/tests/lib/Security/CSRF/TokenStorage/SessionStorageTest.php b/tests/lib/Security/CSRF/TokenStorage/SessionStorageTest.php index 8d4a966efea..2b2c4af0444 100644 --- a/tests/lib/Security/CSRF/TokenStorage/SessionStorageTest.php +++ b/tests/lib/Security/CSRF/TokenStorage/SessionStorageTest.php @@ -1,30 +1,20 @@ <?php + +declare(strict_types=1); + /** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security\CSRF\TokenStorage; +use OC\Security\CSRF\TokenStorage\SessionStorage; use OCP\ISession; class SessionStorageTest extends \Test\TestCase { - /** @var \OCP\ISession */ + /** @var ISession */ private $session; /** @var \OC\Security\CSRF\TokenStorage\SessionStorage */ private $sessionStorage; @@ -33,13 +23,13 @@ class SessionStorageTest extends \Test\TestCase { parent::setUp(); $this->session = $this->getMockBuilder(ISession::class) ->disableOriginalConstructor()->getMock(); - $this->sessionStorage = new \OC\Security\CSRF\TokenStorage\SessionStorage($this->session); + $this->sessionStorage = new SessionStorage($this->session); } /** * @return array */ - public function getTokenDataProvider() { + public static function getTokenDataProvider(): array { return [ [ '', @@ -52,10 +42,10 @@ class SessionStorageTest extends \Test\TestCase { /** * @param string $token - * @dataProvider getTokenDataProvider * */ - public function testGetTokenWithEmptyToken($token) { + #[\PHPUnit\Framework\Attributes\DataProvider('getTokenDataProvider')] + public function testGetTokenWithEmptyToken($token): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Session does not contain a requesttoken'); @@ -67,7 +57,7 @@ class SessionStorageTest extends \Test\TestCase { $this->sessionStorage->getToken(); } - public function testGetTokenWithValidToken() { + public function testGetTokenWithValidToken(): void { $this->session ->expects($this->once()) ->method('get') @@ -76,7 +66,7 @@ class SessionStorageTest extends \Test\TestCase { $this->assertSame('MyFancyCsrfToken', $this->sessionStorage->getToken()); } - public function testSetToken() { + public function testSetToken(): void { $this->session ->expects($this->once()) ->method('set') @@ -84,7 +74,7 @@ class SessionStorageTest extends \Test\TestCase { $this->sessionStorage->setToken('TokenToSet'); } - public function testRemoveToken() { + public function testRemoveToken(): void { $this->session ->expects($this->once()) ->method('remove') @@ -92,7 +82,7 @@ class SessionStorageTest extends \Test\TestCase { $this->sessionStorage->removeToken(); } - public function testHasTokenWithExistingToken() { + public function testHasTokenWithExistingToken(): void { $this->session ->expects($this->once()) ->method('exists') @@ -101,7 +91,7 @@ class SessionStorageTest extends \Test\TestCase { $this->assertSame(true, $this->sessionStorage->hasToken()); } - public function testHasTokenWithoutExistingToken() { + public function testHasTokenWithoutExistingToken(): void { $this->session ->expects($this->once()) ->method('exists') @@ -110,7 +100,7 @@ class SessionStorageTest extends \Test\TestCase { $this->assertSame(false, $this->sessionStorage->hasToken()); } - public function testSetSession() { + public function testSetSession(): void { $session = $this->createMock(ISession::class); $session ->expects($this->once()) diff --git a/tests/lib/Security/CertificateManagerTest.php b/tests/lib/Security/CertificateManagerTest.php index 3af4b564612..4dadc824ef6 100644 --- a/tests/lib/Security/CertificateManagerTest.php +++ b/tests/lib/Security/CertificateManagerTest.php @@ -1,18 +1,27 @@ <?php + +declare(strict_types=1); + /** - * Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security; +use OC\Files\Filesystem; +use OC\Files\Storage\Temporary; use OC\Files\View; +use OC\Security\Certificate; use OC\Security\CertificateManager; +use OCP\Files\InvalidPathException; use OCP\IConfig; -use OCP\ILogger; +use OCP\IUserManager; use OCP\Security\ISecureRandom; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; /** * Class CertificateManagerTest @@ -23,12 +32,9 @@ class CertificateManagerTest extends \Test\TestCase { use \Test\Traits\UserTrait; use \Test\Traits\MountProviderTrait; - /** @var CertificateManager */ - private $certificateManager; - /** @var String */ - private $username; - /** @var ISecureRandom */ - private $random; + private CertificateManager $certificateManager; + private string $username; + private ISecureRandom&MockObject $random; protected function setUp(): void { parent::setUp(); @@ -36,16 +42,16 @@ class CertificateManagerTest extends \Test\TestCase { $this->username = $this->getUniqueID('', 20); $this->createUser($this->username, ''); - $storage = new \OC\Files\Storage\Temporary(); + $storage = new Temporary(); $this->registerMount($this->username, $storage, '/' . $this->username . '/'); \OC_Util::tearDownFS(); - \OC_User::setUserId(''); - \OC\Files\Filesystem::tearDown(); + \OC_User::setUserId($this->username); + Filesystem::tearDown(); \OC_Util::setupFS($this->username); $config = $this->createMock(IConfig::class); - $config->expects($this->any())->method('getSystemValue') + $config->expects($this->any())->method('getSystemValueBool') ->with('installed', false)->willReturn(true); $this->random = $this->createMock(ISecureRandom::class); @@ -53,15 +59,15 @@ class CertificateManagerTest extends \Test\TestCase { ->willReturn('random'); $this->certificateManager = new CertificateManager( - new \OC\Files\View(), + new View(), $config, - $this->createMock(ILogger::class), + $this->createMock(LoggerInterface::class), $this->random ); } protected function tearDown(): void { - $user = \OC::$server->getUserManager()->get($this->username); + $user = Server::get(IUserManager::class)->get($this->username); if ($user !== null) { $user->delete(); } @@ -75,34 +81,31 @@ class CertificateManagerTest extends \Test\TestCase { $this->assertEquals($expected, $actual); } - public function testListCertificates() { + public function testListCertificates(): void { // Test empty certificate bundle $this->assertSame([], $this->certificateManager->listCertificates()); // Add some certificates $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); $certificateStore = []; - $certificateStore[] = new \OC\Security\Certificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); + $certificateStore[] = new Certificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); $this->assertEqualsArrays($certificateStore, $this->certificateManager->listCertificates()); // Add another certificates $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate'); - $certificateStore[] = new \OC\Security\Certificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate'); + $certificateStore[] = new Certificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate'); $this->assertEqualsArrays($certificateStore, $this->certificateManager->listCertificates()); } - public function testAddInvalidCertificate() { + public function testAddInvalidCertificate(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Certificate could not get parsed.'); $this->certificateManager->addCertificate('InvalidCertificate', 'invalidCertificate'); } - /** - * @return array - */ - public function dangerousFileProvider() { + public static function dangerousFileProvider(): array { return [ ['.htaccess'], ['../../foo.txt'], @@ -111,56 +114,54 @@ class CertificateManagerTest extends \Test\TestCase { } /** - * @dataProvider dangerousFileProvider * @param string $filename */ - public function testAddDangerousFile($filename) { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Filename is not valid'); - + #[\PHPUnit\Framework\Attributes\DataProvider('dangerousFileProvider')] + public function testAddDangerousFile($filename): void { + $this->expectException(InvalidPathException::class); $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), $filename); } - public function testRemoveDangerousFile() { + public function testRemoveDangerousFile(): void { $this->assertFalse($this->certificateManager->removeCertificate('../../foo.txt')); } - public function testRemoveExistingFile() { + public function testRemoveExistingFile(): void { $this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate'); $this->assertTrue($this->certificateManager->removeCertificate('GoodCertificate')); } - public function testGetCertificateBundle() { + public function testGetCertificateBundle(): void { $this->assertSame('/files_external/rootcerts.crt', $this->certificateManager->getCertificateBundle()); } /** - * @dataProvider dataTestNeedRebundling * * @param int $CaBundleMtime * @param int $targetBundleMtime * @param int $targetBundleExists * @param bool $expected */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestNeedRebundling')] public function testNeedRebundling($CaBundleMtime, - $targetBundleMtime, - $targetBundleExists, - $expected - ) { + $targetBundleMtime, + $targetBundleExists, + $expected, + ): void { $view = $this->getMockBuilder(View::class) ->disableOriginalConstructor()->getMock(); $config = $this->createMock(IConfig::class); /** @var CertificateManager | \PHPUnit\Framework\MockObject\MockObject $certificateManager */ $certificateManager = $this->getMockBuilder('OC\Security\CertificateManager') - ->setConstructorArgs([$view, $config, $this->createMock(ILogger::class), $this->random]) - ->setMethods(['getFilemtimeOfCaBundle', 'getCertificateBundle']) + ->setConstructorArgs([$view, $config, $this->createMock(LoggerInterface::class), $this->random]) + ->onlyMethods(['getFilemtimeOfCaBundle', 'getCertificateBundle']) ->getMock(); $certificateManager->expects($this->any())->method('getFilemtimeOfCaBundle') ->willReturn($CaBundleMtime); - $certificateManager->expects($this->at(0))->method('getCertificateBundle') + $certificateManager->expects($this->once())->method('getCertificateBundle') ->willReturn('targetBundlePath'); $view->expects($this->any())->method('file_exists') @@ -182,7 +183,7 @@ class CertificateManagerTest extends \Test\TestCase { ); } - public function dataTestNeedRebundling() { + public static function dataTestNeedRebundling(): array { return [ //values: CaBundleMtime, targetBundleMtime, targetBundleExists, expected diff --git a/tests/lib/Security/CertificateTest.php b/tests/lib/Security/CertificateTest.php index 223fb226904..732b431d73e 100644 --- a/tests/lib/Security/CertificateTest.php +++ b/tests/lib/Security/CertificateTest.php @@ -1,22 +1,11 @@ <?php + +declare(strict_types=1); + /** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security; @@ -24,7 +13,6 @@ namespace Test\Security; use OC\Security\Certificate; class CertificateTest extends \Test\TestCase { - /** @var Certificate That contains a valid certificate */ protected $goodCertificate; /** @var Certificate That contains an invalid certificate */ @@ -43,8 +31,8 @@ class CertificateTest extends \Test\TestCase { $this->expiredCertificate = new Certificate($expiredCertificate, 'ExpiredCertificate'); } - - public function testBogusData() { + + public function testBogusData(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Certificate could not get parsed.'); @@ -52,37 +40,42 @@ class CertificateTest extends \Test\TestCase { $certificate->getIssueDate(); } - - public function testCertificateStartingWithFileReference() { + public function testOpenSslTrustedCertificateFormat(): void { + $trustedCertificate = file_get_contents(__DIR__ . '/../../data/certificates/openSslTrustedCertificate.crt'); + $certificate = new Certificate($trustedCertificate, 'TrustedCertificate'); + $this->assertSame('thawte, Inc.', $certificate->getOrganization()); + } + + public function testCertificateStartingWithFileReference(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Certificate could not get parsed.'); - new Certificate('file://'.__DIR__ . '/../../data/certificates/goodCertificate.crt', 'bar'); + new Certificate('file://' . __DIR__ . '/../../data/certificates/goodCertificate.crt', 'bar'); } - public function testGetName() { + public function testGetName(): void { $this->assertSame('GoodCertificate', $this->goodCertificate->getName()); $this->assertSame('BadCertificate', $this->invalidCertificate->getName()); } - public function testGetCommonName() { + public function testGetCommonName(): void { $this->assertSame('security.owncloud.com', $this->goodCertificate->getCommonName()); $this->assertSame(null, $this->invalidCertificate->getCommonName()); } - public function testGetOrganization() { + public function testGetOrganization(): void { $this->assertSame('ownCloud Security', $this->goodCertificate->getOrganization()); $this->assertSame('Internet Widgits Pty Ltd', $this->invalidCertificate->getOrganization()); } - public function testGetIssueDate() { + public function testGetIssueDate(): void { $expected = new \DateTime('2015-08-27 20:03:42 GMT'); $this->assertEquals($expected->getTimestamp(), $this->goodCertificate->getIssueDate()->getTimestamp()); $expected = new \DateTime('2015-08-27 20:19:13 GMT'); $this->assertEquals($expected->getTimestamp(), $this->invalidCertificate->getIssueDate()->getTimestamp()); } - public function testGetExpireDate() { + public function testGetExpireDate(): void { $expected = new \DateTime('2025-08-24 20:03:42 GMT'); $this->assertEquals($expected->getTimestamp(), $this->goodCertificate->getExpireDate()->getTimestamp()); $expected = new \DateTime('2025-08-24 20:19:13 GMT'); @@ -91,19 +84,19 @@ class CertificateTest extends \Test\TestCase { $this->assertEquals($expected->getTimestamp(), $this->expiredCertificate->getExpireDate()->getTimestamp()); } - public function testIsExpired() { + public function testIsExpired(): void { $this->assertSame(false, $this->goodCertificate->isExpired()); $this->assertSame(false, $this->invalidCertificate->isExpired()); $this->assertSame(true, $this->expiredCertificate->isExpired()); } - public function testGetIssuerName() { + public function testGetIssuerName(): void { $this->assertSame('security.owncloud.com', $this->goodCertificate->getIssuerName()); $this->assertSame(null, $this->invalidCertificate->getIssuerName()); $this->assertSame(null, $this->expiredCertificate->getIssuerName()); } - public function testGetIssuerOrganization() { + public function testGetIssuerOrganization(): void { $this->assertSame('ownCloud Security', $this->goodCertificate->getIssuerOrganization()); $this->assertSame('Internet Widgits Pty Ltd', $this->invalidCertificate->getIssuerOrganization()); $this->assertSame('Internet Widgits Pty Ltd', $this->expiredCertificate->getIssuerOrganization()); diff --git a/tests/lib/Security/CredentialsManagerTest.php b/tests/lib/Security/CredentialsManagerTest.php index 3ce80227f44..4dfe8c5681d 100644 --- a/tests/lib/Security/CredentialsManagerTest.php +++ b/tests/lib/Security/CredentialsManagerTest.php @@ -1,140 +1,52 @@ <?php + +declare(strict_types=1); + /** - * @author Robin McCorkell <rmccorkell@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace Test\Security; -use OC\DB\ConnectionAdapter; -use OC\Security\CredentialsManager; -use OC\SystemConfig; -use OCP\IDBConnection; -use OCP\ILogger; -use OCP\Security\ICrypto; +use OCP\Security\ICredentialsManager; +use OCP\Server; /** * @group DB */ class CredentialsManagerTest extends \Test\TestCase { + #[\PHPUnit\Framework\Attributes\DataProvider('credentialsProvider')] + public function testWithDB($userId, $identifier): void { + $credentialsManager = Server::get(ICredentialsManager::class); - /** @var ICrypto */ - protected $crypto; - - /** @var IDBConnection */ - protected $dbConnection; - - /** @var ConnectionAdapter */ - protected $dbConnectionAdapter; - - /** @var CredentialsManager */ - protected $manager; - - protected function setUp(): void { - parent::setUp(); - $this->crypto = $this->createMock(ICrypto::class); - $this->dbConnection = $this->getMockBuilder(IDBConnection::class) - ->disableOriginalConstructor() - ->getMock(); - $this->dbConnectionAdapter = $this->getMockBuilder(ConnectionAdapter::class) - ->disableOriginalConstructor() - ->getMock(); - $this->manager = new CredentialsManager($this->crypto, $this->dbConnection); - } - - private function getQueryResult($row) { - $result = $this->getMockBuilder('\Doctrine\DBAL\Driver\Statement') - ->disableOriginalConstructor() - ->getMock(); - - $result->expects($this->any()) - ->method('fetch') - ->willReturn($row); - - return $result; - } - - public function testStore() { - $userId = 'abc'; - $identifier = 'foo'; - $credentials = 'bar'; - - $this->crypto->expects($this->once()) - ->method('encrypt') - ->with(json_encode($credentials)) - ->willReturn('baz'); - - $this->dbConnection->expects($this->once()) - ->method('setValues') - ->with(CredentialsManager::DB_TABLE, - ['user' => $userId, 'identifier' => $identifier], - ['credentials' => 'baz'] - ); - - $this->manager->store($userId, $identifier, $credentials); - } - - public function testRetrieve() { - $userId = 'abc'; - $identifier = 'foo'; - - $this->crypto->expects($this->once()) - ->method('decrypt') - ->with('baz') - ->willReturn(json_encode('bar')); + $secrets = 'Open Sesame'; - $qb = $this->getMockBuilder(\OC\DB\QueryBuilder\QueryBuilder::class) - ->setConstructorArgs([ - $this->dbConnectionAdapter, - $this->createMock(SystemConfig::class), - $this->createMock(ILogger::class), - ]) - ->setMethods(['execute']) - ->getMock(); - $qb->expects($this->once()) - ->method('execute') - ->willReturn($this->getQueryResult(['credentials' => 'baz'])); + $credentialsManager->store($userId, $identifier, $secrets); + $received = $credentialsManager->retrieve($userId, $identifier); - $this->dbConnectionAdapter->expects($this->once()) - ->method('getQueryBuilder') - ->willReturn($qb); + $this->assertSame($secrets, $received); - $this->manager->retrieve($userId, $identifier); + $removedRows = $credentialsManager->delete($userId, $identifier); + $this->assertSame(1, $removedRows); } - /** - * @dataProvider credentialsProvider - */ - public function testWithDB($userId, $identifier) { - $credentialsManager = \OC::$server->getCredentialsManager(); + #[\PHPUnit\Framework\Attributes\DataProvider('credentialsProvider')] + public function testUpdate($userId, $identifier): void { + $credentialsManager = Server::get(ICredentialsManager::class); $secrets = 'Open Sesame'; + $secretsRev = strrev($secrets); $credentialsManager->store($userId, $identifier, $secrets); + $credentialsManager->store($userId, $identifier, $secretsRev); $received = $credentialsManager->retrieve($userId, $identifier); - $this->assertSame($secrets, $received); - - $removedRows = $credentialsManager->delete($userId, $identifier); - $this->assertSame(1, $removedRows); + $this->assertSame($secretsRev, $received); } - public function credentialsProvider() { + public static function credentialsProvider(): array { return [ [ 'alice', diff --git a/tests/lib/Security/CryptoTest.php b/tests/lib/Security/CryptoTest.php index 0c7c1aa1ac7..0f8575ab0b5 100644 --- a/tests/lib/Security/CryptoTest.php +++ b/tests/lib/Security/CryptoTest.php @@ -1,17 +1,21 @@ <?php + +declare(strict_types=1); + /** - * Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security; use OC\Security\Crypto; +use OCP\IConfig; +use OCP\Server; class CryptoTest extends \Test\TestCase { - public function defaultEncryptionProvider() { + public static function defaultEncryptionProvider(): array { return [ ['Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'], [''], @@ -24,19 +28,17 @@ class CryptoTest extends \Test\TestCase { protected function setUp(): void { parent::setUp(); - $this->crypto = new Crypto(\OC::$server->getConfig()); + $this->crypto = new Crypto(Server::get(IConfig::class)); } - /** - * @dataProvider defaultEncryptionProvider - */ - public function testDefaultEncrypt($stringToEncrypt) { + #[\PHPUnit\Framework\Attributes\DataProvider('defaultEncryptionProvider')] + public function testDefaultEncrypt($stringToEncrypt): void { $ciphertext = $this->crypto->encrypt($stringToEncrypt); $this->assertEquals($stringToEncrypt, $this->crypto->decrypt($ciphertext)); } - public function testWrongPassword() { + public function testWrongPassword(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('HMAC does not match.'); @@ -45,14 +47,14 @@ class CryptoTest extends \Test\TestCase { $this->crypto->decrypt($ciphertext, 'A wrong password!'); } - public function testLaterDecryption() { + public function testLaterDecryption(): void { $stringToEncrypt = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'; $encryptedString = '44a35023cca2e7a6125e06c29fc4b2ad9d8a33d0873a8b45b0de4ef9284f260c6c46bf25dc62120644c59b8bafe4281ddc47a70c35ae6c29ef7a63d79eefacc297e60b13042ac582733598d0a6b4de37311556bb5c480fd2633de4e6ebafa868c2d1e2d80a5d24f9660360dba4d6e0c8|lhrFgK0zd9U160Wo|a75e57ab701f9124e1113543fd1dc596f21e20d456a0d1e813d5a8aaec9adcb11213788e96598b67fe9486a9f0b99642c18296d0175db44b1ae426e4e91080ee'; $this->assertEquals($stringToEncrypt, $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd')); } - public function testWrongIV() { + public function testWrongIV(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('HMAC does not match.'); @@ -61,7 +63,7 @@ class CryptoTest extends \Test\TestCase { } - public function testWrongParameters() { + public function testWrongParameters(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Authenticated ciphertext could not be decoded.'); @@ -69,14 +71,14 @@ class CryptoTest extends \Test\TestCase { $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd'); } - public function testLegacy() { + public function testLegacy(): void { $cipherText = 'e16599188e3d212f5c7f17fdc2abca46|M1WfLAxbcAmITeD6|509457885d6ca5e6c3bfd3741852687a7f2bffce197f8d5ae97b65818b15a1b7f616b68326ff312371540f4ca8ac55f8e2de4aa13aab3474bd3431e51214e3ee'; $password = 'mypass'; $this->assertSame('legacy test', $this->crypto->decrypt($cipherText, $password)); } - public function testVersion2CiphertextDecryptsToCorrectPlaintext() { + public function testVersion2CiphertextDecryptsToCorrectPlaintext(): void { $this->assertSame( 'This is a plaintext value that will be encrypted with version 2. Which addresses the reduced permutations on the IV.', $this->crypto->decrypt( @@ -86,7 +88,20 @@ class CryptoTest extends \Test\TestCase { ); } - public function testVersion3CiphertextDecryptsToCorrectPlaintext() { + /** + * Test data taken from https://github.com/owncloud/core/blob/9deb8196b20354c8de0cd720ad4d18d52ccc96d8/tests/lib/Security/CryptoTest.php#L56-L60 + */ + public function testOcVersion2CiphertextDecryptsToCorrectPlaintext(): void { + $this->assertSame( + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.', + $this->crypto->decrypt( + 'v2|d57dbe4d1317cdf19d4ddc2df807f6b5d63ab1e119c46590ce54bae56a9cd3969168c4ec1600ac9758dd7e7afb9c4c962dd23072c1463add1d9c77c467723b37bb768ef00e3c50898e59247cbb59ce56b74ce5990648ffe9e40d0e95076c27a785bdcf32c219ea4ad5c316b1f12f48c1|6bd21db258a5e406a2c288a444de195f|a19111a4cf1a11ee95fc1734699c20964eaa05bb007e1cecc4cc6872f827a4b7deedc977c13b138d728d68116aa3d82f9673e20c7e447a9788aa3be994b67cd6', + 'ThisIsAVeryS3cur3P4ssw0rd' + ) + ); + } + + public function testVersion3CiphertextDecryptsToCorrectPlaintext(): void { $this->assertSame( 'Another plaintext value that will be encrypted with version 3. It addresses the related key issue. Old ciphertexts should be decrypted properly, but only use the better version for encryption.', $this->crypto->decrypt( diff --git a/tests/lib/Security/Events/GenerateSecurePasswordEventTest.php b/tests/lib/Security/Events/GenerateSecurePasswordEventTest.php new file mode 100644 index 00000000000..e589f20f62c --- /dev/null +++ b/tests/lib/Security/Events/GenerateSecurePasswordEventTest.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Security\Events; + +use OCP\Security\Events\GenerateSecurePasswordEvent; +use OCP\Security\PasswordContext; + +class GenerateSecurePasswordEventTest extends \Test\TestCase { + + public function testDefaultProperties(): void { + $event = new GenerateSecurePasswordEvent(); + $this->assertNull($event->getPassword()); + $this->assertEquals(PasswordContext::ACCOUNT, $event->getContext()); + } + + public function testSettingPassword(): void { + $event = new GenerateSecurePasswordEvent(); + $event->setPassword('example'); + $this->assertEquals('example', $event->getPassword()); + } + + public function testSettingContext(): void { + $event = new GenerateSecurePasswordEvent(PasswordContext::SHARING); + $this->assertEquals(PasswordContext::SHARING, $event->getContext()); + } +} diff --git a/tests/lib/Security/Events/ValidatePasswordPolicyEventTest.php b/tests/lib/Security/Events/ValidatePasswordPolicyEventTest.php new file mode 100644 index 00000000000..272f34e31aa --- /dev/null +++ b/tests/lib/Security/Events/ValidatePasswordPolicyEventTest.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Security\Events; + +use OCP\Security\Events\ValidatePasswordPolicyEvent; +use OCP\Security\PasswordContext; + +class ValidatePasswordPolicyEventTest extends \Test\TestCase { + + public function testDefaultProperties(): void { + $password = 'example'; + $event = new ValidatePasswordPolicyEvent($password); + $this->assertEquals($password, $event->getPassword()); + $this->assertEquals(PasswordContext::ACCOUNT, $event->getContext()); + } + + public function testSettingContext(): void { + $event = new ValidatePasswordPolicyEvent('example', PasswordContext::SHARING); + $this->assertEquals(PasswordContext::SHARING, $event->getContext()); + } +} diff --git a/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php b/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php index 6e6433adbeb..1fe01e26280 100644 --- a/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php +++ b/tests/lib/Security/FeaturePolicy/AddFeaturePolicyEventTest.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\CSP; @@ -31,7 +14,7 @@ use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; use Test\TestCase; class AddFeaturePolicyEventTest extends TestCase { - public function testAddEvent() { + public function testAddEvent(): void { $manager = $this->createMock(FeaturePolicyManager::class); $policy = $this->createMock(FeaturePolicy::class); $event = new AddFeaturePolicyEvent($manager); diff --git a/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php b/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php index b8f558719dc..01624cb92d3 100644 --- a/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php +++ b/tests/lib/Security/FeaturePolicy/FeaturePolicyManagerTest.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\CSP; @@ -29,11 +12,11 @@ use OC\Security\FeaturePolicy\FeaturePolicyManager; use OCP\AppFramework\Http\FeaturePolicy; use OCP\EventDispatcher\IEventDispatcher; use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use OCP\Server; use Test\TestCase; class FeaturePolicyManagerTest extends TestCase { - /** @var EventDispatcherInterface */ + /** @var IEventDispatcher */ private $dispatcher; /** @var FeaturePolicyManager */ @@ -41,17 +24,17 @@ class FeaturePolicyManagerTest extends TestCase { protected function setUp(): void { parent::setUp(); - $this->dispatcher = \OC::$server->query(IEventDispatcher::class); + $this->dispatcher = Server::get(IEventDispatcher::class); $this->manager = new FeaturePolicyManager($this->dispatcher); } - public function testAddDefaultPolicy() { + public function testAddDefaultPolicy(): void { $this->manager->addDefaultPolicy(new FeaturePolicy()); $this->addToAssertionCount(1); } - public function testGetDefaultPolicyWithPoliciesViaEvent() { - $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e) { + public function testGetDefaultPolicyWithPoliciesViaEvent(): void { + $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e): void { $policy = new FeaturePolicy(); $policy->addAllowedMicrophoneDomain('mydomain.com'); $policy->addAllowedPaymentDomain('mypaymentdomain.com'); @@ -59,7 +42,7 @@ class FeaturePolicyManagerTest extends TestCase { $e->addPolicy($policy); }); - $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e) { + $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e): void { $policy = new FeaturePolicy(); $policy->addAllowedPaymentDomain('mydomainother.com'); $policy->addAllowedGeoLocationDomain('mylocation.here'); @@ -67,7 +50,7 @@ class FeaturePolicyManagerTest extends TestCase { $e->addPolicy($policy); }); - $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e) { + $this->dispatcher->addListener(AddFeaturePolicyEvent::class, function (AddFeaturePolicyEvent $e): void { $policy = new FeaturePolicy(); $policy->addAllowedAutoplayDomain('youtube.com'); diff --git a/tests/lib/Security/HasherTest.php b/tests/lib/Security/HasherTest.php index 3bd98625529..33130f86a73 100644 --- a/tests/lib/Security/HasherTest.php +++ b/tests/lib/Security/HasherTest.php @@ -1,9 +1,11 @@ <?php + +declare(strict_types=1); + /** - * Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security; @@ -15,11 +17,7 @@ use OCP\IConfig; * Class HasherTest */ class HasherTest extends \Test\TestCase { - - /** - * @return array - */ - public function versionHashProvider() { + public static function versionHashProvider(): array { return [ ['asf32äà$$a.|3', null], ['asf32äà$$a.|3|5', null], @@ -29,7 +27,7 @@ class HasherTest extends \Test\TestCase { ]; } - public function hashProviders70_71(): array { + public static function hashProviders70_71(): array { return [ // Valid SHA1 strings ['password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true], @@ -65,7 +63,7 @@ class HasherTest extends \Test\TestCase { ]; } - public function hashProviders72(): array { + public static function hashProviders72(): array { return [ // Valid ARGON2 hashes ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$T3JGcEkxVFNOVktNSjZUcg$4/hyLtSejxNgAuzSFFV/HLM3qRQKBwEtKw61qPN4zWA', true], @@ -82,7 +80,7 @@ class HasherTest extends \Test\TestCase { ]; } - public function hashProviders73(): array { + public static function hashProviders73(): array { return [ // Valid ARGON2ID hashes ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$TEtIMnhUczliQzI0Y01WeA$BpMUDrApy25iagIogUAnlc0rNTPJmGs8lOEeVHujJ9Q', true], @@ -121,24 +119,20 @@ class HasherTest extends \Test\TestCase { $this->hasher = new Hasher($this->config); } - public function testHash() { + public function testHash(): void { $hash = $this->hasher->hash('String To Hash'); $this->assertNotNull($hash); } - /** - * @dataProvider versionHashProvider - */ - public function testSplitHash($hash, $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('versionHashProvider')] + public function testSplitHash($hash, $expected): void { $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]); $this->assertSame($expected, $relativePath); } - /** - * @dataProvider hashProviders70_71 - */ - public function testVerify($password, $hash, $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('hashProviders70_71')] + public function testVerify($password, $hash, $expected): void { $this->config ->expects($this->any()) ->method('getSystemValue') @@ -153,10 +147,8 @@ class HasherTest extends \Test\TestCase { $this->assertSame($expected, $result); } - /** - * @dataProvider hashProviders72 - */ - public function testVerifyArgon2i($password, $hash, $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('hashProviders72')] + public function testVerifyArgon2i($password, $hash, $expected): void { if (!\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); } @@ -165,10 +157,8 @@ class HasherTest extends \Test\TestCase { $this->assertSame($expected, $result); } - /** - * @dataProvider hashProviders73 - */ - public function testVerifyArgon2id(string $password, string $hash, bool $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('hashProviders73')] + public function testVerifyArgon2id(string $password, string $hash, bool $expected): void { if (!\defined('PASSWORD_ARGON2ID')) { $this->markTestSkipped('Need ARGON2ID support to test ARGON2ID hashes'); } @@ -177,7 +167,7 @@ class HasherTest extends \Test\TestCase { $this->assertSame($expected, $result); } - public function testUpgradeHashBlowFishToArgon2() { + public function testUpgradeHashBlowFishToArgon2(): void { if (!\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); } @@ -194,7 +184,7 @@ class HasherTest extends \Test\TestCase { } - $this->assertTrue($this->hasher->verify($message, $blowfish,$newHash)); + $this->assertTrue($this->hasher->verify($message, $blowfish, $newHash)); $this->assertTrue($this->hasher->verify($message, $argon2)); $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$newHash]); @@ -202,12 +192,12 @@ class HasherTest extends \Test\TestCase { $this->assertFalse(password_needs_rehash($relativePath['hash'], $newAlg, [])); } - public function testUsePasswordDefaultArgon2iVerify() { + public function testUsePasswordDefaultArgon2iVerify(): void { if (!\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); } - $this->config->method('getSystemValue') + $this->config->method('getSystemValueBool') ->with('hashing_default_password') ->willReturn(true); @@ -226,12 +216,12 @@ class HasherTest extends \Test\TestCase { $this->assertTrue(password_verify($message, $relativePath['hash'])); } - public function testDoNotUsePasswordDefaultArgon2idVerify() { + public function testDoNotUsePasswordDefaultArgon2idVerify(): void { if (!\defined('PASSWORD_ARGON2ID')) { $this->markTestSkipped('Need ARGON2ID support to test ARGON2ID hashes'); } - $this->config->method('getSystemValue') + $this->config->method('getSystemValueBool') ->with('hashing_default_password') ->willReturn(false); @@ -244,12 +234,12 @@ class HasherTest extends \Test\TestCase { $this->assertNull($newHash); } - public function testHashUsePasswordDefault() { + public function testHashUsePasswordDefault(): void { if (!\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); } - $this->config->method('getSystemValue') + $this->config->method('getSystemValueBool') ->with('hashing_default_password') ->willReturn(true); @@ -263,4 +253,29 @@ class HasherTest extends \Test\TestCase { $info = password_get_info($relativePath['hash']); $this->assertEquals(PASSWORD_BCRYPT, $info['algo']); } + + public function testValidHash(): void { + $hash = '3|$argon2id$v=19$m=65536,t=4,p=1$czFCSjk3LklVdXppZ2VCWA$li0NgdXe2/jwSRxgteGQPWlzJU0E0xdtfHbCbrpych0'; + + $isValid = $this->hasher->validate($hash); + + $this->assertTrue($isValid); + } + + public function testValidGeneratedHash(): void { + $message = 'secret'; + $hash = $this->hasher->hash($message); + + $isValid = $this->hasher->validate($hash); + + $this->assertTrue($isValid); + } + + public function testInvalidHash(): void { + $invalidHash = 'someInvalidHash'; + + $isValid = $this->hasher->validate($invalidHash); + + $this->assertFalse($isValid); + } } diff --git a/tests/lib/Security/IdentityProof/KeyTest.php b/tests/lib/Security/IdentityProof/KeyTest.php index ae5f77f1609..572bfbea619 100644 --- a/tests/lib/Security/IdentityProof/KeyTest.php +++ b/tests/lib/Security/IdentityProof/KeyTest.php @@ -1,24 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\IdentityProof; @@ -36,11 +22,11 @@ class KeyTest extends TestCase { $this->key = new Key('public', 'private'); } - public function testGetPrivate() { + public function testGetPrivate(): void { $this->assertSame('private', $this->key->getPrivate()); } - public function testGetPublic() { + public function testGetPublic(): void { $this->assertSame('public', $this->key->getPublic()); } } diff --git a/tests/lib/Security/IdentityProof/ManagerTest.php b/tests/lib/Security/IdentityProof/ManagerTest.php index 760c4911873..921d72388a1 100644 --- a/tests/lib/Security/IdentityProof/ManagerTest.php +++ b/tests/lib/Security/IdentityProof/ManagerTest.php @@ -1,22 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\IdentityProof; @@ -28,26 +16,24 @@ use OC\Security\IdentityProof\Manager; use OCP\Files\IAppData; use OCP\Files\SimpleFS\ISimpleFile; use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; -use OCP\ILogger; use OCP\IUser; use OCP\Security\ICrypto; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use Test\TestCase; class ManagerTest extends TestCase { - /** @var Factory|MockObject */ - private $factory; - /** @var IAppData|MockObject */ - private $appData; - /** @var ICrypto|MockObject */ - private $crypto; - /** @var Manager|MockObject */ - private $manager; - /** @var IConfig|MockObject */ - private $config; - /** @var ILogger|MockObject */ - private $logger; + private Factory&MockObject $factory; + private IAppData&MockObject $appData; + private ICrypto&MockObject $crypto; + private Manager&MockObject $manager; + private IConfig&MockObject $config; + private LoggerInterface&MockObject $logger; + private ICacheFactory&MockObject $cacheFactory; + private ICache&MockObject $cache; protected function setUp(): void { parent::setUp(); @@ -60,7 +46,13 @@ class ManagerTest extends TestCase { ->method('get') ->with('identityproof') ->willReturn($this->appData); - $this->logger = $this->createMock(ILogger::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cache = $this->createMock(ICache::class); + + $this->cacheFactory->expects($this->any()) + ->method('createDistributed') + ->willReturn($this->cache); $this->crypto = $this->createMock(ICrypto::class); $this->manager = $this->getManager(['generateKeyPair']); @@ -78,7 +70,8 @@ class ManagerTest extends TestCase { $this->factory, $this->crypto, $this->config, - $this->logger + $this->logger, + $this->cacheFactory, ); } else { return $this->getMockBuilder(Manager::class) @@ -86,12 +79,15 @@ class ManagerTest extends TestCase { $this->factory, $this->crypto, $this->config, - $this->logger - ])->setMethods($setMethods)->getMock(); + $this->logger, + $this->cacheFactory, + ]) + ->onlyMethods($setMethods) + ->getMock(); } } - public function testGetKeyWithExistingKey() { + public function testGetKeyWithExistingKey(): void { $user = $this->createMock(IUser::class); $user ->expects($this->once()) @@ -114,42 +110,61 @@ class ManagerTest extends TestCase { ->with('EncryptedPrivateKey') ->willReturn('MyPrivateKey'); $folder - ->expects($this->at(0)) - ->method('getFile') - ->with('private') - ->willReturn($privateFile); - $folder - ->expects($this->at(1)) + ->expects($this->exactly(2)) ->method('getFile') - ->with('public') - ->willReturn($publicFile); + ->willReturnMap([ + ['private', $privateFile], + ['public', $publicFile], + ]); $this->appData ->expects($this->once()) ->method('getFolder') ->with('user-MyUid') ->willReturn($folder); + $this->cache + ->expects($this->exactly(2)) + ->method('get') + ->willReturn(null); $expected = new Key('MyPublicKey', 'MyPrivateKey'); $this->assertEquals($expected, $this->manager->getKey($user)); } - public function testGetKeyWithNotExistingKey() { + public function testGetKeyWithExistingKeyCached(): void { + $user = $this->createMock(IUser::class); + $user + ->expects($this->once()) + ->method('getUID') + ->willReturn('MyUid'); + $this->crypto + ->expects($this->once()) + ->method('decrypt') + ->with('EncryptedPrivateKey') + ->willReturn('MyPrivateKey'); + $this->cache + ->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['user-MyUid-public', 'MyPublicKey'], + ['user-MyUid-private', 'EncryptedPrivateKey'], + ]); + + $expected = new Key('MyPublicKey', 'MyPrivateKey'); + $this->assertEquals($expected, $this->manager->getKey($user)); + } + + public function testGetKeyWithNotExistingKey(): void { $user = $this->createMock(IUser::class); $user ->expects($this->once()) ->method('getUID') ->willReturn('MyUid'); - $this->appData - ->expects($this->at(0)) - ->method('getFolder') - ->with('user-MyUid') - ->willThrowException(new \Exception()); $this->manager ->expects($this->once()) ->method('generateKeyPair') ->willReturn(['MyNewPublicKey', 'MyNewPrivateKey']); $this->appData - ->expects($this->at(1)) + ->expects($this->once()) ->method('newFolder') ->with('user-MyUid'); $folder = $this->createMock(ISimpleFolder::class); @@ -169,31 +184,31 @@ class ManagerTest extends TestCase { ->method('putContent') ->with('MyNewPublicKey'); $folder - ->expects($this->at(0)) + ->expects($this->exactly(2)) ->method('newFile') - ->with('private') - ->willReturn($privateFile); - $folder - ->expects($this->at(1)) - ->method('newFile') - ->with('public') - ->willReturn($publicFile); + ->willReturnMap([ + ['private', null, $privateFile], + ['public', null, $publicFile], + ]); $this->appData - ->expects($this->at(2)) + ->expects($this->exactly(2)) ->method('getFolder') ->with('user-MyUid') - ->willReturn($folder); + ->willReturnOnConsecutiveCalls( + $this->throwException(new \Exception()), + $folder + ); $expected = new Key('MyNewPublicKey', 'MyNewPrivateKey'); $this->assertEquals($expected, $this->manager->getKey($user)); } - public function testGenerateKeyPair() { + public function testGenerateKeyPair(): void { $manager = $this->getManager(); $data = 'MyTestData'; - list($resultPublicKey, $resultPrivateKey) = self::invokePrivate($manager, 'generateKeyPair'); + [$resultPublicKey, $resultPrivateKey] = self::invokePrivate($manager, 'generateKeyPair'); openssl_sign($data, $signature, $resultPrivateKey); $details = openssl_pkey_get_details(openssl_pkey_get_public($resultPublicKey)); @@ -201,7 +216,7 @@ class ManagerTest extends TestCase { $this->assertSame(2048, $details['bits']); } - public function testGetSystemKey() { + public function testGetSystemKey(): void { $manager = $this->getManager(['retrieveKey']); /** @var Key|\PHPUnit\Framework\MockObject\MockObject $key */ @@ -218,7 +233,7 @@ class ManagerTest extends TestCase { - public function testGetSystemKeyFailure() { + public function testGetSystemKeyFailure(): void { $this->expectException(\RuntimeException::class); $manager = $this->getManager(['retrieveKey']); diff --git a/tests/lib/Security/IdentityProof/SignerTest.php b/tests/lib/Security/IdentityProof/SignerTest.php index 3ce211d51e7..6dc73c072e4 100644 --- a/tests/lib/Security/IdentityProof/SignerTest.php +++ b/tests/lib/Security/IdentityProof/SignerTest.php @@ -1,24 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\IdentityProof; @@ -32,7 +18,6 @@ use OCP\IUserManager; use Test\TestCase; class SignerTest extends TestCase { - /** @var string */ private $private = '-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDImc6QvHBjBIoo @@ -105,7 +90,7 @@ gQIDAQAB ); } - public function testSign() { + public function testSign(): void { $user = $this->createMock(IUser::class); $user->method('getCloudId') ->willReturn('foo@example.com'); @@ -138,7 +123,7 @@ gQIDAQAB $this->assertEquals($expects, $result); } - public function testVerifyValid() { + public function testVerifyValid(): void { $data = [ 'message' => [ 'data' => [ @@ -166,7 +151,7 @@ gQIDAQAB $this->assertTrue($this->signer->verify($data)); } - public function testVerifyInvalid() { + public function testVerifyInvalid(): void { $data = [ 'message' => [ 'data' => [ @@ -194,7 +179,7 @@ gQIDAQAB $this->assertFalse($this->signer->verify($data)); } - public function testVerifyInvalidData() { + public function testVerifyInvalidData(): void { $data = [ ]; diff --git a/tests/lib/Security/Ip/BruteforceAllowListTest.php b/tests/lib/Security/Ip/BruteforceAllowListTest.php new file mode 100644 index 00000000000..1454b779c1b --- /dev/null +++ b/tests/lib/Security/Ip/BruteforceAllowListTest.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Security\Ip; + +use OC\Security\Ip\BruteforceAllowList; +use OC\Security\Ip\Factory; +use OCP\IAppConfig; +use OCP\Security\Ip\IFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +/** + * Based on the unit tests from Paragonie's Airship CMS + * Ref: https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/test/unit/Engine/Security/AirBrakeTest.php + * + * @package Test\Security\Bruteforce + */ +class BruteforceAllowListTest extends TestCase { + /** @var IAppConfig|MockObject */ + private $appConfig; + /** @var IFactory|MockObject */ + private $factory; + /** @var BruteforceAllowList */ + private $allowList; + + protected function setUp(): void { + parent::setUp(); + + $this->appConfig = $this->createMock(IAppConfig::class); + $this->factory = new Factory(); + + $this->allowList = new BruteforceAllowList( + $this->appConfig, + $this->factory, + ); + } + + public static function dataIsBypassListed(): array { + return [ + [ + '10.10.10.10', + [ + 'whitelist_0' => '10.10.10.0/24', + ], + true, + ], + [ + '10.10.10.10', + [ + 'whitelist_0' => '192.168.0.0/16', + ], + false, + ], + [ + '10.10.10.10', + [ + 'whitelist_0' => '192.168.0.0/16', + 'whitelist_1' => '10.10.10.0/24', + ], + true, + ], + [ + '10.10.10.10', + [ + 'whitelist_0' => '10.10.10.11/31', + ], + true, + ], + [ + '10.10.10.10', + [ + 'whitelist_0' => '10.10.10.9/31', + ], + false, + ], + [ + '10.10.10.10', + [ + 'whitelist_0' => '10.10.10.15/29', + ], + true, + ], + [ + 'dead:beef:cafe::1', + [ + 'whitelist_0' => '192.168.0.0/16', + 'whitelist_1' => '10.10.10.0/24', + 'whitelist_2' => 'deaf:beef:cafe:1234::/64' + ], + false, + ], + [ + 'dead:beef:cafe::1', + [ + 'whitelist_0' => '192.168.0.0/16', + 'whitelist_1' => '10.10.10.0/24', + 'whitelist_2' => 'deaf:beef::/64' + ], + false, + ], + [ + 'dead:beef:cafe::1', + [ + 'whitelist_0' => '192.168.0.0/16', + 'whitelist_1' => '10.10.10.0/24', + 'whitelist_2' => 'deaf:cafe::/8' + ], + true, + ], + [ + 'dead:beef:cafe::1111', + [ + 'whitelist_0' => 'dead:beef:cafe::1100/123', + ], + true, + ], + [ + 'invalid', + [], + false, + ], + ]; + } + + /** + * @param string[] $allowList + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataIsBypassListed')] + public function testIsBypassListed( + string $ip, + array $allowList, + bool $isAllowListed, + ): void { + $this->appConfig->method('searchKeys') + ->with($this->equalTo('bruteForce'), $this->equalTo('whitelist_')) + ->willReturn(array_keys($allowList)); + + $this->appConfig->method('getValueString') + ->willReturnCallback(function ($app, $key, $default) use ($allowList) { + if ($app !== 'bruteForce') { + return $default; + } + if (isset($allowList[$key])) { + return $allowList[$key]; + } + return $default; + }); + + $this->assertSame( + $isAllowListed, + $this->allowList->isBypassListed($ip) + ); + } +} diff --git a/tests/lib/Security/Ip/RemoteAddressTest.php b/tests/lib/Security/Ip/RemoteAddressTest.php new file mode 100644 index 00000000000..a6619cffe8e --- /dev/null +++ b/tests/lib/Security/Ip/RemoteAddressTest.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Security\Ip; + +use OC\Security\Ip\RemoteAddress; +use OCP\IConfig; +use OCP\IRequest; + +class RemoteAddressTest extends \Test\TestCase { + private IConfig $config; + private IRequest $request; + + protected function setUp(): void { + parent::setUp(); + $this->config = $this->createMock(IConfig::class); + $this->request = $this->createMock(IRequest::class); + } + + /** + * @param mixed $allowedRanges + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')] + public function testAllowedIps(string $remoteIp, $allowedRanges, bool $expected): void { + $this->request + ->method('getRemoteAddress') + ->willReturn($remoteIp); + $this->config + ->method('getSystemValue') + ->with('allowed_admin_ranges', false) + ->willReturn($allowedRanges); + + $remoteAddress = new RemoteAddress($this->config, $this->request); + + $this->assertEquals($expected, $remoteAddress->allowsAdminActions()); + } + + /** + * @return array<string, mixed, bool> + */ + public static function dataProvider(): array { + return [ + // No IP (ie. CLI) + ['', ['192.168.1.2/24'], true], + ['', ['fe80/8'], true], + // No configuration + ['1.2.3.4', false, true], + ['1234:4567:8910::', false, true], + // v6 Zone ID + ['fe80::1fc4:15d8:78db:2319%enp4s0', false, true], + // Empty configuration + ['1.2.3.4', [], true], + ['1234:4567:8910::', [], true], + // Invalid configuration + ['1.2.3.4', 'hello', true], + ['1234:4567:8910::', 'world', true], + // Mixed configuration + ['192.168.1.5', ['1.2.3.*', '1234::/8'], false], + ['::1', ['127.0.0.1', '1234::/8'], false], + ['192.168.1.5', ['192.168.1.0/24', '1234::/8'], true], + // Allowed IP + ['1.2.3.4', ['1.2.3.*'], true], + ['fc00:1:2:3::1', ['fc00::/7'], true], + ['1.2.3.4', ['192.168.1.2/24', '1.2.3.0/24'], true], + ['1234:4567:8910::1', ['fe80::/8','1234:4567::/16'], true], + // Blocked IP + ['192.168.1.5', ['1.2.3.*'], false], + ['9234:4567:8910::', ['1234:4567::1'], false], + ['192.168.2.1', ['192.168.1.2/24', '1.2.3.0/24'], false], + ['9234:4567:8910::', ['fe80::/8','1234:4567::/16'], false], + ]; + } +} diff --git a/tests/lib/Security/Normalizer/IpAddressTest.php b/tests/lib/Security/Normalizer/IpAddressTest.php index 16be71cb225..f7adfb4a0dd 100644 --- a/tests/lib/Security/Normalizer/IpAddressTest.php +++ b/tests/lib/Security/Normalizer/IpAddressTest.php @@ -1,22 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\Normalizer; @@ -25,7 +13,7 @@ use OC\Security\Normalizer\IpAddress; use Test\TestCase; class IpAddressTest extends TestCase { - public function subnetDataProvider() { + public static function subnetDataProvider(): array { return [ [ '64.233.191.254', @@ -36,27 +24,47 @@ class IpAddressTest extends TestCase { '192.168.0.123/32', ], [ - '2001:0db8:85a3:0000:0000:8a2e:0370:7334', - '2001:db8:85a3::8a2e:370:7334/128', + '::ffff:192.168.0.123', + '192.168.0.123/32', + ], + [ + '0:0:0:0:0:ffff:192.168.0.123', + '192.168.0.123/32', + ], + [ + '0:0:0:0:0:ffff:c0a8:7b', + '192.168.0.123/32', + ], + [ + '2001:0db8:0000:0000:0000:8a2e:0370:7334', + '2001:db8::/56', + ], + [ + '2001:db8:3333:4444:5555:6666:7777:8888', + '2001:db8:3333:4400::/56', + ], + [ + '::1234:5678', + '::/56', ], [ '[::1]', - '::1/128', + '::/56', ], ]; } /** - * @dataProvider subnetDataProvider * * @param string $input * @param string $expected */ - public function testGetSubnet($input, $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('subnetDataProvider')] + public function testGetSubnet($input, $expected): void { $this->assertSame($expected, (new IpAddress($input))->getSubnet()); } - public function testToString() { + public function testToString(): void { $this->assertSame('127.0.0.1', (string)(new IpAddress('127.0.0.1'))); } } diff --git a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php b/tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php index 902c586dc13..24e3ab1a209 100644 --- a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php +++ b/tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php @@ -1,45 +1,37 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\RateLimiting\Backend; -use OC\Security\RateLimiting\Backend\MemoryCache; +use OC\Security\RateLimiting\Backend\MemoryCacheBackend; use OCP\AppFramework\Utility\ITimeFactory; use OCP\ICache; use OCP\ICacheFactory; +use OCP\IConfig; use Test\TestCase; -class MemoryCacheTest extends TestCase { +class MemoryCacheBackendTest extends TestCase { + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + private $config; /** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */ private $cacheFactory; /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ private $timeFactory; /** @var ICache|\PHPUnit\Framework\MockObject\MockObject */ private $cache; - /** @var MemoryCache */ + /** @var MemoryCacheBackend */ private $memoryCache; protected function setUp(): void { parent::setUp(); + $this->config = $this->createMock(IConfig::class); $this->cacheFactory = $this->createMock(ICacheFactory::class); $this->timeFactory = $this->createMock(ITimeFactory::class); $this->cache = $this->createMock(ICache::class); @@ -47,26 +39,31 @@ class MemoryCacheTest extends TestCase { $this->cacheFactory ->expects($this->once()) ->method('createDistributed') - ->with('OC\Security\RateLimiting\Backend\MemoryCache') + ->with('OC\Security\RateLimiting\Backend\MemoryCacheBackend') ->willReturn($this->cache); - $this->memoryCache = new MemoryCache( + $this->config->method('getSystemValueBool') + ->with('ratelimit.protection.enabled') + ->willReturn(true); + + $this->memoryCache = new MemoryCacheBackend( + $this->config, $this->cacheFactory, $this->timeFactory ); } - public function testGetAttemptsWithNoAttemptsBefore() { + public function testGetAttemptsWithNoAttemptsBefore(): void { $this->cache ->expects($this->once()) ->method('get') ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') ->willReturn(null); - $this->assertSame(0, $this->memoryCache->getAttempts('Method', 'User', 123)); + $this->assertSame(0, $this->memoryCache->getAttempts('Method', 'User')); } - public function testGetAttempts() { + public function testGetAttempts(): void { $this->timeFactory ->expects($this->once()) ->method('getTime') @@ -79,15 +76,15 @@ class MemoryCacheTest extends TestCase { '1', '2', '87', - '123', - '123', - '124', + '223', + '223', + '224', ])); - $this->assertSame(3, $this->memoryCache->getAttempts('Method', 'User', 123)); + $this->assertSame(3, $this->memoryCache->getAttempts('Method', 'User')); } - public function testRegisterAttemptWithNoAttemptsBefore() { + public function testRegisterAttemptWithNoAttemptsBefore(): void { $this->timeFactory ->expects($this->once()) ->method('getTime') @@ -103,17 +100,17 @@ class MemoryCacheTest extends TestCase { ->method('set') ->with( 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', - json_encode(['123']) + json_encode(['223']) ); $this->memoryCache->registerAttempt('Method', 'User', 100); } - public function testRegisterAttempt() { + public function testRegisterAttempt(): void { $this->timeFactory ->expects($this->once()) ->method('getTime') - ->willReturn(129); + ->willReturn(86); $this->cache ->expects($this->once()) @@ -137,7 +134,7 @@ class MemoryCacheTest extends TestCase { '123', '123', '124', - '129', + '186', ]) ); diff --git a/tests/lib/Security/RateLimiting/LimiterTest.php b/tests/lib/Security/RateLimiting/LimiterTest.php index 76121a49bc1..b19d5c6feba 100644 --- a/tests/lib/Security/RateLimiting/LimiterTest.php +++ b/tests/lib/Security/RateLimiting/LimiterTest.php @@ -1,55 +1,44 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security\RateLimiting; use OC\Security\RateLimiting\Backend\IBackend; +use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; -use OCP\AppFramework\Utility\ITimeFactory; use OCP\IUser; +use OCP\Security\RateLimiting\ILimiter; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use Test\TestCase; class LimiterTest extends TestCase { - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ - private $timeFactory; - /** @var IBackend|\PHPUnit\Framework\MockObject\MockObject */ - private $backend; - /** @var Limiter */ - private $limiter; + + private IBackend&MockObject $backend; + private ILimiter $limiter; + private LoggerInterface $logger; protected function setUp(): void { parent::setUp(); - $this->timeFactory = $this->createMock(ITimeFactory::class); $this->backend = $this->createMock(IBackend::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->limiter = new Limiter( - $this->timeFactory, - $this->backend + $this->backend, + $this->logger, ); } - public function testRegisterAnonRequestExceeded() { - $this->expectException(\OC\Security\RateLimiting\Exception\RateLimitExceededException::class); + public function testRegisterAnonRequestExceeded(): void { + $this->expectException(RateLimitExceededException::class); $this->expectExceptionMessage('Rate limit exceeded'); $this->backend @@ -57,26 +46,22 @@ class LimiterTest extends TestCase { ->method('getAttempts') ->with( 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 100 + '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47' ) ->willReturn(101); + $this->logger->expects($this->once()) + ->method('info'); $this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); } - public function testRegisterAnonRequestSuccess() { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(2000); + public function testRegisterAnonRequestSuccess(): void { $this->backend ->expects($this->once()) ->method('getAttempts') ->with( 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 100 + '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47' ) ->willReturn(99); $this->backend @@ -85,15 +70,17 @@ class LimiterTest extends TestCase { ->with( 'MyIdentifier', '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 2000 + 100 ); + $this->logger->expects($this->never()) + ->method('info'); $this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); } - public function testRegisterUserRequestExceeded() { - $this->expectException(\OC\Security\RateLimiting\Exception\RateLimitExceededException::class); + public function testRegisterUserRequestExceeded(): void { + $this->expectException(RateLimitExceededException::class); $this->expectExceptionMessage('Rate limit exceeded'); /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ @@ -107,15 +94,16 @@ class LimiterTest extends TestCase { ->method('getAttempts') ->with( 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 100 + 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805' ) ->willReturn(101); + $this->logger->expects($this->once()) + ->method('info'); $this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); } - public function testRegisterUserRequestSuccess() { + public function testRegisterUserRequestSuccess(): void { /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ $user = $this->createMock(IUser::class); $user @@ -123,17 +111,12 @@ class LimiterTest extends TestCase { ->method('getUID') ->willReturn('MyUid'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(2000); $this->backend ->expects($this->once()) ->method('getAttempts') ->with( 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 100 + 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805' ) ->willReturn(99); $this->backend @@ -142,8 +125,10 @@ class LimiterTest extends TestCase { ->with( 'MyIdentifier', 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 2000 + 100 ); + $this->logger->expects($this->never()) + ->method('info'); $this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); } diff --git a/tests/lib/Security/RemoteHostValidatorIntegrationTest.php b/tests/lib/Security/RemoteHostValidatorIntegrationTest.php new file mode 100644 index 00000000000..913acfa054d --- /dev/null +++ b/tests/lib/Security/RemoteHostValidatorIntegrationTest.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Security; + +use OC\Net\HostnameClassifier; +use OC\Net\IpAddressClassifier; +use OC\Security\RemoteHostValidator; +use OCP\IConfig; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\NullLogger; +use Test\TestCase; + +class RemoteHostValidatorIntegrationTest extends TestCase { + /** @var IConfig|IConfig&MockObject|MockObject */ + private IConfig $config; + private RemoteHostValidator $validator; + + protected function setUp(): void { + parent::setUp(); + + // Mock config to avoid any side effects + $this->config = $this->createMock(IConfig::class); + + $this->validator = new RemoteHostValidator( + $this->config, + Server::get(HostnameClassifier::class), + Server::get(IpAddressClassifier::class), + new NullLogger(), + ); + } + + public static function localHostsData(): array { + return [ + ['[::1]'], + ['[::]'], + ['192.168.0.1'], + ['172.16.42.1'], + ['[fdf8:f53b:82e4::53]'], + ['[fe80::200:5aee:feaa:20a2]'], + ['[0:0:0:0:0:ffff:10.0.0.1]'], + ['[0:0:0:0:0:ffff:127.0.0.0]'], + ['10.0.0.1'], + ['!@#$'], // test invalid url + ['100.100.100.200'], + ['192.0.0.1'], + ['0177.0.0.9'], + ['⑯⑨。②⑤④。⑯⑨。②⑤④'], + ['127。②⑤④。⑯⑨.②⑤④'], + ['127.0.00000000000000000000000000000000001'], + ['127.1'], + ['127.000.001'], + ['0177.0.0.01'], + ['0x7f.0x0.0x0.0x1'], + ['0x7f000001'], + ['2130706433'], + ['00000000000000000000000000000000000000000000000000177.1'], + ['0x7f.1'], + ['127.0x1'], + ['[0000:0000:0000:0000:0000:0000:0000:0001]'], + ['[0:0:0:0:0:0:0:1]'], + ['[0:0:0:0::0:0:1]'], + ['%31%32%37%2E%30%2E%30%2E%31'], + ['%31%32%37%2E%30%2E%30.%31'], + ['[%3A%3A%31]'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('localHostsData')] + public function testLocalHostsWhenNotAllowed(string $host): void { + $this->config + ->method('getSystemValueBool') + ->with('allow_local_remote_servers', false) + ->willReturn(false); + + $isValid = $this->validator->isValid($host); + + self::assertFalse($isValid); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('localHostsData')] + public function testLocalHostsWhenAllowed(string $host): void { + $this->config + ->method('getSystemValueBool') + ->with('allow_local_remote_servers', false) + ->willReturn(true); + + $isValid = $this->validator->isValid($host); + + self::assertTrue($isValid); + } + + public static function externalAddressesData():array { + return [ + ['8.8.8.8'], + ['8.8.4.4'], + ['8.8.8.8'], + ['8.8.4.4'], + ['[2001:4860:4860::8888]'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('externalAddressesData')] + public function testExternalHost(string $host): void { + $this->config + ->method('getSystemValueBool') + ->with('allow_local_remote_servers', false) + ->willReturn(false); + + $isValid = $this->validator->isValid($host); + + self::assertTrue($isValid); + } +} diff --git a/tests/lib/Security/RemoteHostValidatorTest.php b/tests/lib/Security/RemoteHostValidatorTest.php new file mode 100644 index 00000000000..b048b9dafd1 --- /dev/null +++ b/tests/lib/Security/RemoteHostValidatorTest.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Security; + +use OC\Net\HostnameClassifier; +use OC\Net\IpAddressClassifier; +use OC\Security\RemoteHostValidator; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class RemoteHostValidatorTest extends TestCase { + /** @var IConfig|IConfig&MockObject|MockObject */ + private IConfig $config; + /** @var HostnameClassifier|HostnameClassifier&MockObject|MockObject */ + private HostnameClassifier $hostnameClassifier; + /** @var IpAddressClassifier|IpAddressClassifier&MockObject|MockObject */ + private IpAddressClassifier $ipAddressClassifier; + /** @var MockObject|LoggerInterface|LoggerInterface&MockObject */ + private LoggerInterface $logger; + private RemoteHostValidator $validator; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->hostnameClassifier = $this->createMock(HostnameClassifier::class); + $this->ipAddressClassifier = $this->createMock(IpAddressClassifier::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->validator = new RemoteHostValidator( + $this->config, + $this->hostnameClassifier, + $this->ipAddressClassifier, + $this->logger, + ); + } + + public static function dataValid(): array { + return [ + ['nextcloud.com', true], + ['com.one-.nextcloud-one.com', false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataValid')] + public function testValid(string $host, bool $expected): void { + $this->hostnameClassifier + ->method('isLocalHostname') + ->with($host) + ->willReturn(false); + $this->ipAddressClassifier + ->method('isLocalAddress') + ->with($host) + ->willReturn(false); + + $valid = $this->validator->isValid($host); + + self::assertSame($expected, $valid); + } + + public function testLocalHostname(): void { + $host = 'localhost'; + $this->hostnameClassifier + ->method('isLocalHostname') + ->with($host) + ->willReturn(true); + $this->ipAddressClassifier + ->method('isLocalAddress') + ->with($host) + ->willReturn(false); + + $valid = $this->validator->isValid($host); + + self::assertFalse($valid); + } + + public function testLocalAddress(): void { + $host = '10.0.0.10'; + $this->hostnameClassifier + ->method('isLocalHostname') + ->with($host) + ->willReturn(false); + $this->ipAddressClassifier + ->method('isLocalAddress') + ->with($host) + ->willReturn(true); + + $valid = $this->validator->isValid($host); + + self::assertFalse($valid); + } +} diff --git a/tests/lib/Security/SecureRandomTest.php b/tests/lib/Security/SecureRandomTest.php index 0ffd7ae7c14..954fd85eaf1 100644 --- a/tests/lib/Security/SecureRandomTest.php +++ b/tests/lib/Security/SecureRandomTest.php @@ -1,9 +1,11 @@ <?php + +declare(strict_types=1); + /** - * Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security; @@ -11,9 +13,8 @@ namespace Test\Security; use OC\Security\SecureRandom; class SecureRandomTest extends \Test\TestCase { - public function stringGenerationProvider() { + public static function stringGenerationProvider(): array { return [ - [0, 0], [1, 1], [128, 128], [256, 256], @@ -23,7 +24,7 @@ class SecureRandomTest extends \Test\TestCase { ]; } - public static function charCombinations() { + public static function charCombinations(): array { return [ ['CHAR_LOWER', '[a-z]'], ['CHAR_UPPER', '[A-Z]'], @@ -36,42 +37,48 @@ class SecureRandomTest extends \Test\TestCase { protected function setUp(): void { parent::setUp(); - $this->rng = new \OC\Security\SecureRandom(); + $this->rng = new SecureRandom(); } - /** - * @dataProvider stringGenerationProvider - */ - public function testGetLowStrengthGeneratorLength($length, $expectedLength) { + #[\PHPUnit\Framework\Attributes\DataProvider('stringGenerationProvider')] + public function testGetLowStrengthGeneratorLength($length, $expectedLength): void { $generator = $this->rng; $this->assertEquals($expectedLength, strlen($generator->generate($length))); } - /** - * @dataProvider stringGenerationProvider - */ - public function testMediumLowStrengthGeneratorLength($length, $expectedLength) { + #[\PHPUnit\Framework\Attributes\DataProvider('stringGenerationProvider')] + public function testMediumLowStrengthGeneratorLength($length, $expectedLength): void { $generator = $this->rng; $this->assertEquals($expectedLength, strlen($generator->generate($length))); } - /** - * @dataProvider stringGenerationProvider - */ - public function testUninitializedGenerate($length, $expectedLength) { + #[\PHPUnit\Framework\Attributes\DataProvider('stringGenerationProvider')] + public function testUninitializedGenerate($length, $expectedLength): void { $this->assertEquals($expectedLength, strlen($this->rng->generate($length))); } - /** - * @dataProvider charCombinations - */ - public function testScheme($charName, $chars) { + #[\PHPUnit\Framework\Attributes\DataProvider('charCombinations')] + public function testScheme($charName, $chars): void { $generator = $this->rng; $scheme = constant('OCP\Security\ISecureRandom::' . $charName); $randomString = $generator->generate(100, $scheme); - $matchesRegex = preg_match('/^'.$chars.'+$/', $randomString); + $matchesRegex = preg_match('/^' . $chars . '+$/', $randomString); $this->assertSame(1, $matchesRegex); } + + public static function invalidLengths(): array { + return [ + [0], + [-1], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('invalidLengths')] + public function testInvalidLengths($length): void { + $this->expectException(\LengthException::class); + $generator = $this->rng; + $generator->generate($length); + } } diff --git a/tests/lib/Security/TrustedDomainHelperTest.php b/tests/lib/Security/TrustedDomainHelperTest.php index 2796dead0e2..8b671a93d06 100644 --- a/tests/lib/Security/TrustedDomainHelperTest.php +++ b/tests/lib/Security/TrustedDomainHelperTest.php @@ -1,9 +1,11 @@ <?php + +declare(strict_types=1); + /** - * Copyright (c) 2015 Lukas Reschke <lukas@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace Test\Security; @@ -25,20 +27,34 @@ class TrustedDomainHelperTest extends \Test\TestCase { } /** - * @dataProvider trustedDomainDataProvider * @param string $trustedDomains * @param string $testDomain * @param bool $result */ - public function testIsTrustedDomain($trustedDomains, $testDomain, $result) { - $this->config->expects($this->at(0)) - ->method('getSystemValue') - ->with('overwritehost') - ->willReturn(''); - $this->config->expects($this->at(1)) - ->method('getSystemValue') - ->with('trusted_domains') - ->willReturn($trustedDomains); + #[\PHPUnit\Framework\Attributes\DataProvider('trustedDomainDataProvider')] + public function testIsTrustedUrl($trustedDomains, $testDomain, $result): void { + $this->config->method('getSystemValue') + ->willReturnMap([ + ['overwritehost', '', ''], + ['trusted_domains', [], $trustedDomains], + ]); + + $trustedDomainHelper = new TrustedDomainHelper($this->config); + $this->assertEquals($result, $trustedDomainHelper->isTrustedUrl('https://' . $testDomain . '/index.php/something')); + } + + /** + * @param string $trustedDomains + * @param string $testDomain + * @param bool $result + */ + #[\PHPUnit\Framework\Attributes\DataProvider('trustedDomainDataProvider')] + public function testIsTrustedDomain($trustedDomains, $testDomain, $result): void { + $this->config->method('getSystemValue') + ->willReturnMap([ + ['overwritehost', '', ''], + ['trusted_domains', [], $trustedDomains], + ]); $trustedDomainHelper = new TrustedDomainHelper($this->config); $this->assertEquals($result, $trustedDomainHelper->isTrustedDomain($testDomain)); @@ -47,7 +63,7 @@ class TrustedDomainHelperTest extends \Test\TestCase { /** * @return array */ - public function trustedDomainDataProvider() { + public static function trustedDomainDataProvider(): array { $trustedHostTestList = [ 'host.one.test', 'host.two.test', @@ -118,9 +134,8 @@ class TrustedDomainHelperTest extends \Test\TestCase { ]; } - public function testIsTrustedDomainOverwriteHost() { - $this->config->expects($this->at(0)) - ->method('getSystemValue') + public function testIsTrustedDomainOverwriteHost(): void { + $this->config->method('getSystemValue') ->with('overwritehost') ->willReturn('myproxyhost'); diff --git a/tests/lib/Security/VerificationToken/VerificationTokenTest.php b/tests/lib/Security/VerificationToken/VerificationTokenTest.php new file mode 100644 index 00000000000..7123cc8b013 --- /dev/null +++ b/tests/lib/Security/VerificationToken/VerificationTokenTest.php @@ -0,0 +1,295 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Security\VerificationToken; + +use OC\Security\VerificationToken\VerificationToken; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IUser; +use OCP\Security\ICrypto; +use OCP\Security\ISecureRandom; +use OCP\Security\VerificationToken\InvalidTokenException; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class VerificationTokenTest extends TestCase { + /** @var VerificationToken */ + protected $token; + /** @var IConfig|MockObject */ + protected $config; + /** @var ISecureRandom|MockObject */ + protected $secureRandom; + /** @var ICrypto|MockObject */ + protected $crypto; + /** @var ITimeFactory|MockObject */ + protected $timeFactory; + /** @var IJobList|MockObject */ + protected $jobList; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->jobList = $this->createMock(IJobList::class); + + $this->token = new VerificationToken( + $this->config, + $this->crypto, + $this->timeFactory, + $this->secureRandom, + $this->jobList + ); + } + + public function testTokenUserUnknown(): void { + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::USER_UNKNOWN); + $this->token->check('encryptedToken', null, 'fingerprintToken', 'foobar'); + } + + public function testTokenUserUnknown2(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(false); + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::USER_UNKNOWN); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); + } + + public function testTokenNotFound(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + + // implicit: IConfig::getUserValue returns null by default + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::TOKEN_NOT_FOUND); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); + } + + public function testTokenDecryptionError(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('alice', 'core', 'fingerprintToken', null) + ->willReturn('encryptedToken'); + $this->config->expects($this->any()) + ->method('getSystemValueString') + ->with('secret') + ->willReturn('357111317'); + + $this->crypto->method('decrypt') + ->with('encryptedToken', 'foobar' . '357111317') + ->willThrowException(new \Exception('decryption failed')); + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::TOKEN_DECRYPTION_ERROR); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); + } + + public function testTokenInvalidFormat(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('alice', 'core', 'fingerprintToken', null) + ->willReturn('encryptedToken'); + $this->config->expects($this->any()) + ->method('getSystemValueString') + ->with('secret') + ->willReturn('357111317'); + + $this->crypto->method('decrypt') + ->with('encryptedToken', 'foobar' . '357111317') + ->willReturn('decrypted^nonsense'); + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::TOKEN_INVALID_FORMAT); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); + } + + public function testTokenExpired(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + $user->expects($this->any()) + ->method('getLastLogin') + ->willReturn(604803); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('alice', 'core', 'fingerprintToken', null) + ->willReturn('encryptedToken'); + $this->config->expects($this->any()) + ->method('getSystemValueString') + ->with('secret') + ->willReturn('357111317'); + + $this->crypto->method('decrypt') + ->with('encryptedToken', 'foobar' . '357111317') + ->willReturn('604800:mY70K3n'); + + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(604800 * 3); + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::TOKEN_EXPIRED); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); + } + + public function testTokenExpiredByLogin(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + $user->expects($this->any()) + ->method('getLastLogin') + ->willReturn(604803); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('alice', 'core', 'fingerprintToken', null) + ->willReturn('encryptedToken'); + $this->config->expects($this->any()) + ->method('getSystemValueString') + ->with('secret') + ->willReturn('357111317'); + + $this->crypto->method('decrypt') + ->with('encryptedToken', 'foobar' . '357111317') + ->willReturn('604800:mY70K3n'); + + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(604801); + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::TOKEN_EXPIRED); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar', true); + } + + public function testTokenMismatch(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + $user->expects($this->any()) + ->method('getLastLogin') + ->willReturn(604703); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('alice', 'core', 'fingerprintToken', null) + ->willReturn('encryptedToken'); + $this->config->expects($this->any()) + ->method('getSystemValueString') + ->with('secret') + ->willReturn('357111317'); + + $this->crypto->method('decrypt') + ->with('encryptedToken', 'foobar' . '357111317') + ->willReturn('604802:mY70K3n'); + + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(604801); + + $this->expectException(InvalidTokenException::class); + $this->expectExceptionCode(InvalidTokenException::TOKEN_MISMATCH); + $this->token->check('encryptedToken', $user, 'fingerprintToken', 'foobar'); + } + + public function testTokenSuccess(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->atLeastOnce()) + ->method('getUID') + ->willReturn('alice'); + $user->expects($this->any()) + ->method('getLastLogin') + ->willReturn(604703); + + $this->config->expects($this->atLeastOnce()) + ->method('getUserValue') + ->with('alice', 'core', 'fingerprintToken', null) + ->willReturn('encryptedToken'); + $this->config->expects($this->any()) + ->method('getSystemValueString') + ->with('secret') + ->willReturn('357111317'); + + $this->crypto->method('decrypt') + ->with('encryptedToken', 'foobar' . '357111317') + ->willReturn('604802:barfoo'); + + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(604801); + + $this->token->check('barfoo', $user, 'fingerprintToken', 'foobar'); + } + + public function testCreate(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('alice'); + + $this->secureRandom->expects($this->atLeastOnce()) + ->method('generate') + ->willReturn('barfoo'); + $this->crypto->expects($this->atLeastOnce()) + ->method('encrypt') + ->willReturn('encryptedToken'); + $this->config->expects($this->atLeastOnce()) + ->method('setUserValue') + ->with('alice', 'core', 'fingerprintToken', 'encryptedToken'); + + $vToken = $this->token->create($user, 'fingerprintToken', 'foobar'); + $this->assertSame('barfoo', $vToken); + } +} |