Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/dav/lib/Connector/LegacyPublicAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

use OCA\DAV\Connector\Sabre\PublicAuth;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Share\Events\ShareLinkAccessFailedEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
Expand All @@ -36,6 +38,7 @@ public function __construct(
private ISession $session,
private IThrottler $throttler,
private IUserSession $userSession,
private IEventDispatcher $eventDispatcher,
) {
// setup realm
$defaults = new Defaults();
Expand All @@ -62,6 +65,7 @@ protected function validateUserPass($username, $password) {
$share = $this->shareManager->getShareByToken($username);
} catch (ShareNotFound $e) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($username, null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND));
return false;
}

Expand All @@ -88,6 +92,8 @@ protected function validateUserPass($username, $password) {
return true;
}

$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($username, $share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD));

if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')), true)) {
// do not re-authenticate over ajax, use dummy auth name to prevent browser popup
http_response_code(401);
Expand Down
7 changes: 7 additions & 0 deletions apps/dav/lib/Connector/Sabre/PublicAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
namespace OCA\DAV\Connector\Sabre;

use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
use OCP\Share\Events\ShareLinkAccessFailedEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
Expand Down Expand Up @@ -49,6 +51,7 @@ public function __construct(
private LoggerInterface $logger,
private IURLGenerator $urlGenerator,
private IUserSession $userSession,
private IEventDispatcher $eventDispatcher,
) {
// setup realm
$defaults = new Defaults();
Expand Down Expand Up @@ -132,6 +135,7 @@ private function checkToken(): array {
$share = $this->shareManager->getShareByToken($token);
} catch (ShareNotFound $e) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($token, null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND));
throw new NotFound();
}

Expand Down Expand Up @@ -172,6 +176,7 @@ protected function validateUserPass($username, $password) {
$share = $this->getShare();
} catch (ShareNotFound $e) {
$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());
$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($this->getToken(), null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND));
return false;
}

Expand All @@ -196,6 +201,8 @@ protected function validateUserPass($username, $password) {
return true;
}

$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($this->getToken(), $share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD));

if (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')), true)) {
// do not re-authenticate over ajax, use dummy auth name to prevent browser popup
http_response_code(401);
Expand Down
18 changes: 18 additions & 0 deletions apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
namespace OCA\DAV\Tests\unit\Connector;

use OCA\DAV\Connector\LegacyPublicAuth;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Share\Events\ShareLinkAccessFailedEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
Expand All @@ -28,6 +30,7 @@ class LegacyPublicAuthTest extends TestCase {
private IManager&MockObject $shareManager;
private IThrottler&MockObject $throttler;
private IUserSession&MockObject $userSession;
private IEventDispatcher&MockObject $eventDispatcher;
private LegacyPublicAuth $auth;
private string|false $oldUser;

Expand All @@ -39,13 +42,15 @@ protected function setUp(): void {
$this->shareManager = $this->createMock(IManager::class);
$this->throttler = $this->createMock(IThrottler::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);

$this->auth = new LegacyPublicAuth(
$this->request,
$this->shareManager,
$this->session,
$this->throttler,
$this->userSession,
$this->eventDispatcher,
);

// Store current user
Expand All @@ -68,6 +73,7 @@ public function testNoShare(): void {
$this->shareManager->expects($this->once())
->method('getShareByToken')
->willThrowException(new ShareNotFound());
$this->expectFailureEvent('username', null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND);

$result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand Down Expand Up @@ -133,6 +139,7 @@ public function testSharePasswordLinkValidPassword(): void {
$this->equalTo($share),
$this->equalTo('password')
)->willReturn(true);
$this->eventDispatcher->expects($this->never())->method('dispatchTyped');

$result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand All @@ -154,6 +161,7 @@ public function testSharePasswordMailValidPassword(): void {
$this->equalTo($share),
$this->equalTo('password')
)->willReturn(true);
$this->eventDispatcher->expects($this->never())->method('dispatchTyped');

$result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand Down Expand Up @@ -204,6 +212,7 @@ public function testSharePasswordLinkInvalidSession(): void {

$this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
$this->session->method('get')->with('public_link_authenticated')->willReturn(['43']);
$this->expectFailureEvent('username', $share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD);

$result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand All @@ -229,9 +238,18 @@ public function testSharePasswordMailInvalidSession(): void {

$this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
$this->session->method('get')->with('public_link_authenticated')->willReturn(['43']);
$this->expectFailureEvent('username', $share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD);

$result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

$this->assertFalse($result);
}

private function expectFailureEvent(string $token, ?IShare $share, string $reason): void {
$this->eventDispatcher->expects($this->once())
->method('dispatchTyped')
->with($this->callback(static fn (ShareLinkAccessFailedEvent $event): bool => $event->getToken() === $token
&& $event->getShare() === $share
&& $event->getReason() === $reason));
}
}
19 changes: 19 additions & 0 deletions apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
namespace OCA\DAV\Tests\unit\Connector;

use OCA\DAV\Connector\Sabre\PublicAuth;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Share\Events\ShareLinkAccessFailedEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
Expand All @@ -37,6 +39,7 @@ class PublicAuthTest extends \Test\TestCase {
private LoggerInterface&MockObject $logger;
private IURLGenerator&MockObject $urlGenerator;
private IUserSession&MockObject $userSession;
private IEventDispatcher&MockObject $eventDispatcher;
private PublicAuth $auth;

private bool|string $oldUser;
Expand All @@ -51,6 +54,7 @@ protected function setUp(): void {
$this->logger = $this->createMock(LoggerInterface::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);

$this->auth = new PublicAuth(
$this->request,
Expand All @@ -60,6 +64,7 @@ protected function setUp(): void {
$this->logger,
$this->urlGenerator,
$this->userSession,
$this->eventDispatcher,
);

// Store current user
Expand Down Expand Up @@ -121,6 +126,7 @@ public function testCheckTokenInvalidShare(): void {
->method('getShareByToken')
->with('GX9HSGQrGE')
->willThrowException(new ShareNotFound());
$this->expectFailureEvent('GX9HSGQrGE', null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND);

$this->expectException(\Sabre\DAV\Exception\NotFound::class);
self::invokePrivate($this->auth, 'checkToken');
Expand Down Expand Up @@ -194,6 +200,7 @@ public function testNoShare(): void {
->method('getShareByToken')
->with('GX9HSGQrGE')
->willThrowException(new ShareNotFound());
$this->expectFailureEvent('GX9HSGQrGE', null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND);

$result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand Down Expand Up @@ -275,6 +282,7 @@ public function testSharePasswordLinkValidPassword(): void {
$this->equalTo($share),
$this->equalTo('password')
)->willReturn(true);
$this->eventDispatcher->expects($this->never())->method('dispatchTyped');

$result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand All @@ -300,6 +308,7 @@ public function testSharePasswordMailValidPassword(): void {
$this->equalTo($share),
$this->equalTo('password')
)->willReturn(true);
$this->eventDispatcher->expects($this->never())->method('dispatchTyped');

$result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand Down Expand Up @@ -360,6 +369,7 @@ public function testSharePasswordLinkInvalidSession(): void {

$this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
$this->session->method('get')->with('public_link_authenticated')->willReturn('43');
$this->expectFailureEvent('GX9HSGQrGE', $share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD);

$result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

Expand Down Expand Up @@ -390,9 +400,18 @@ public function testSharePasswordMailInvalidSession(): void {

$this->session->method('exists')->with('public_link_authenticated')->willReturn(true);
$this->session->method('get')->with('public_link_authenticated')->willReturn('43');
$this->expectFailureEvent('GX9HSGQrGE', $share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD);

$result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']);

$this->assertFalse($result);
}

private function expectFailureEvent(string $token, ?IShare $share, string $reason): void {
$this->eventDispatcher->expects($this->once())
->method('dispatchTyped')
->with($this->callback(static fn (ShareLinkAccessFailedEvent $event): bool => $event->getToken() === $token
&& $event->getShare() === $share
&& $event->getReason() === $reason));
}
}
7 changes: 7 additions & 0 deletions apps/files_sharing/lib/Controller/ShareController.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
use OCP\Security\ISecureRandom;
use OCP\Security\PasswordContext;
use OCP\Share;
use OCP\Share\Events\ShareLinkAccessFailedEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager as ShareManager;
use OCP\Share\IPublicShareTemplateFactory;
Expand Down Expand Up @@ -200,6 +201,12 @@ protected function authSucceeded() {
protected function authFailed() {
$this->emitAccessShareHook($this->share, 403, 'Wrong password');
$this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($this->getToken(), $this->share, ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD));
}

#[\Override]
public function shareNotFound() {
$this->eventDispatcher->dispatchTyped(new ShareLinkAccessFailedEvent($this->getToken(), null, ShareLinkAccessFailedEvent::REASON_NOT_FOUND));
}

/**
Expand Down
37 changes: 37 additions & 0 deletions apps/files_sharing/tests/Controller/ShareControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OCP\AppFramework\Services\IInitialState;
use OCP\Constants;
use OCP\Defaults;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\IRootFolder;
Expand All @@ -46,6 +47,7 @@
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use OCP\Server;
use OCP\Share\Events\ShareLinkAccessFailedEvent;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IAttributes;
use OCP\Share\IPublicShareTemplateFactory;
Expand Down Expand Up @@ -193,6 +195,41 @@ public function testShowShareInvalidToken(): void {
$this->shareController->showShare();
}

public function testShareNotFoundDispatchesFailureEvent(): void {
$this->shareController->setToken('invalidtoken');

$this->eventDispatcher->expects($this->once())
->method('dispatchTyped')
->with($this->callback(static fn (ShareLinkAccessFailedEvent $event): bool => $event->getToken() === 'invalidtoken'
&& $event->getShare() === null
&& $event->getReason() === ShareLinkAccessFailedEvent::REASON_NOT_FOUND));

$this->shareController->shareNotFound();
}

public function testAuthFailedDispatchesFailureEvent(): void {
$this->shareController->setToken('token');
$share = $this->createMock(IShare::class);
$this->shareManager->method('getShareByToken')
->with('token')
->willReturn($share);
$this->assertTrue($this->shareController->isValidToken());

$events = [];
$this->eventDispatcher->method('dispatchTyped')
->willReturnCallback(static function (Event $event) use (&$events): void {
$events[] = $event;
});

self::invokePrivate($this->shareController, 'authFailed');

$failures = array_values(array_filter($events, static fn (Event $event): bool => $event instanceof ShareLinkAccessFailedEvent));
$this->assertCount(1, $failures);
$this->assertSame('token', $failures[0]->getToken());
$this->assertSame($share, $failures[0]->getShare());
$this->assertSame(ShareLinkAccessFailedEvent::REASON_WRONG_PASSWORD, $failures[0]->getReason());
}

public function testShowShareNotAuthenticated(): void {
$this->shareController->setToken('validtoken');

Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
'OCP\\Share\\Events\\ShareCreatedEvent' => $baseDir . '/lib/public/Share/Events/ShareCreatedEvent.php',
'OCP\\Share\\Events\\ShareDeletedEvent' => $baseDir . '/lib/public/Share/Events/ShareDeletedEvent.php',
'OCP\\Share\\Events\\ShareDeletedFromSelfEvent' => $baseDir . '/lib/public/Share/Events/ShareDeletedFromSelfEvent.php',
'OCP\\Share\\Events\\ShareLinkAccessFailedEvent' => $baseDir . '/lib/public/Share/Events/ShareLinkAccessFailedEvent.php',
'OCP\\Share\\Events\\ShareMovedEvent' => $baseDir . '/lib/public/Share/Events/ShareMovedEvent.php',
'OCP\\Share\\Events\\ShareTransferredEvent' => $baseDir . '/lib/public/Share/Events/ShareTransferredEvent.php',
'OCP\\Share\\Events\\VerifyMountPointEvent' => $baseDir . '/lib/public/Share/Events/VerifyMountPointEvent.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Share\\Events\\ShareCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareCreatedEvent.php',
'OCP\\Share\\Events\\ShareDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareDeletedEvent.php',
'OCP\\Share\\Events\\ShareDeletedFromSelfEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareDeletedFromSelfEvent.php',
'OCP\\Share\\Events\\ShareLinkAccessFailedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareLinkAccessFailedEvent.php',
'OCP\\Share\\Events\\ShareMovedEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareMovedEvent.php',
'OCP\\Share\\Events\\ShareTransferredEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/ShareTransferredEvent.php',
'OCP\\Share\\Events\\VerifyMountPointEvent' => __DIR__ . '/../../..' . '/lib/public/Share/Events/VerifyMountPointEvent.php',
Expand Down
Loading
Loading