Skip to content

Commit 920a741

Browse files
authored
Merge pull request #47403 from nextcloud/feat/password-context
feat(Security): Allow defining a password context for password validation and generation
2 parents d78bfb7 + dd58e52 commit 920a741

File tree

10 files changed

+184
-63
lines changed

10 files changed

+184
-63
lines changed

apps/files_sharing/lib/Controller/ShareController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@
3333
use OCP\ISession;
3434
use OCP\IURLGenerator;
3535
use OCP\IUserManager;
36+
use OCP\Security\Events\GenerateSecurePasswordEvent;
3637
use OCP\Security\ISecureRandom;
38+
use OCP\Security\PasswordContext;
3739
use OCP\Share;
3840
use OCP\Share\Exceptions\ShareNotFound;
3941
use OCP\Share\IManager as ShareManager;
4042
use OCP\Share\IPublicShareTemplateFactory;
4143
use OCP\Share\IShare;
42-
use OCP\Template;
4344

4445
/**
4546
* @package OCA\Files_Sharing\Controllers
@@ -156,7 +157,7 @@ protected function validateIdentity(?string $identityToken = null): bool {
156157
* Generates a password for the share, respecting any password policy defined
157158
*/
158159
protected function generatePassword(): void {
159-
$event = new \OCP\Security\Events\GenerateSecurePasswordEvent();
160+
$event = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
160161
$this->eventDispatcher->dispatchTyped($event);
161162
$password = $event->getPassword() ?? $this->secureRandom->generate(20);
162163

apps/sharebymail/lib/ShareByMailProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use OCP\Security\Events\GenerateSecurePasswordEvent;
2929
use OCP\Security\IHasher;
3030
use OCP\Security\ISecureRandom;
31+
use OCP\Security\PasswordContext;
3132
use OCP\Share\Exceptions\GenericShareException;
3233
use OCP\Share\Exceptions\ShareNotFound;
3334
use OCP\Share\IAttributes;
@@ -131,7 +132,7 @@ protected function autoGeneratePassword(IShare $share): string {
131132
);
132133
}
133134

134-
$passwordEvent = new GenerateSecurePasswordEvent();
135+
$passwordEvent = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
135136
$this->eventDispatcher->dispatchTyped($passwordEvent);
136137

137138
$password = $passwordEvent->getPassword();

apps/sharebymail/tests/ShareByMailProviderTest.php

Lines changed: 36 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
use OC\Mail\Message;
1010
use OCA\ShareByMail\Settings\SettingsManager;
1111
use OCA\ShareByMail\ShareByMailProvider;
12+
use OCP\Activity\IManager as IActivityManager;
1213
use OCP\Defaults;
1314
use OCP\EventDispatcher\IEventDispatcher;
1415
use OCP\Files\File;
1516
use OCP\Files\IRootFolder;
17+
use OCP\Files\Node;
1618
use OCP\IConfig;
1719
use OCP\IDBConnection;
1820
use OCP\IL10N;
@@ -25,9 +27,11 @@
2527
use OCP\Security\Events\GenerateSecurePasswordEvent;
2628
use OCP\Security\IHasher;
2729
use OCP\Security\ISecureRandom;
30+
use OCP\Security\PasswordContext;
2831
use OCP\Share\IAttributes;
2932
use OCP\Share\IManager;
3033
use OCP\Share\IShare;
34+
use PHPUnit\Framework\MockObject\MockObject;
3135
use Psr\Log\LoggerInterface;
3236
use Test\TestCase;
3337

@@ -38,65 +42,36 @@
3842
* @group DB
3943
*/
4044
class ShareByMailProviderTest extends TestCase {
41-
/** @var IConfig */
42-
private $config;
43-
44-
/** @var IDBConnection */
45-
private $connection;
46-
47-
/** @var IManager | \PHPUnit\Framework\MockObject\MockObject */
48-
private $shareManager;
49-
50-
/** @var IL10N | \PHPUnit\Framework\MockObject\MockObject */
51-
private $l;
52-
53-
/** @var LoggerInterface | \PHPUnit\Framework\MockObject\MockObject */
54-
private $logger;
55-
56-
/** @var IRootFolder | \PHPUnit\Framework\MockObject\MockObject */
57-
private $rootFolder;
58-
59-
/** @var IUserManager | \PHPUnit\Framework\MockObject\MockObject */
60-
private $userManager;
61-
62-
/** @var ISecureRandom | \PHPUnit\Framework\MockObject\MockObject */
63-
private $secureRandom;
64-
65-
/** @var IMailer | \PHPUnit\Framework\MockObject\MockObject */
66-
private $mailer;
67-
68-
/** @var IURLGenerator | \PHPUnit\Framework\MockObject\MockObject */
69-
private $urlGenerator;
70-
71-
/** @var IShare | \PHPUnit\Framework\MockObject\MockObject */
72-
private $share;
73-
74-
/** @var \OCP\Activity\IManager | \PHPUnit\Framework\MockObject\MockObject */
75-
private $activityManager;
76-
77-
/** @var SettingsManager | \PHPUnit\Framework\MockObject\MockObject */
78-
private $settingsManager;
79-
80-
/** @var Defaults|\PHPUnit\Framework\MockObject\MockObject */
81-
private $defaults;
82-
83-
/** @var IHasher | \PHPUnit\Framework\MockObject\MockObject */
84-
private $hasher;
85-
86-
/** @var IEventDispatcher */
87-
private $eventDispatcher;
45+
46+
private IDBConnection $connection;
47+
48+
private IL10N&MockObject $l;
49+
private IShare&MockObject $share;
50+
private IConfig&MockObject $config;
51+
private IMailer&MockObject $mailer;
52+
private IHasher&MockObject $hasher;
53+
private Defaults&MockObject $defaults;
54+
private IManager&MockObject $shareManager;
55+
private LoggerInterface&MockObject $logger;
56+
private IRootFolder&MockObject $rootFolder;
57+
private IUserManager&MockObject $userManager;
58+
private ISecureRandom&MockObject $secureRandom;
59+
private IURLGenerator&MockObject $urlGenerator;
60+
private SettingsManager&MockObject $settingsManager;
61+
private IActivityManager&MockObject $activityManager;
62+
private IEventDispatcher&MockObject $eventDispatcher;
8863

8964
protected function setUp(): void {
9065
parent::setUp();
9166

92-
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
93-
$this->connection = \OC::$server->getDatabaseConnection();
67+
$this->connection = \OCP\Server::get(IDBConnection::class);
9468

9569
$this->l = $this->getMockBuilder(IL10N::class)->getMock();
9670
$this->l->method('t')
9771
->willReturnCallback(function ($text, $parameters = []) {
9872
return vsprintf($text, $parameters);
9973
});
74+
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
10075
$this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
10176
$this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock();
10277
$this->userManager = $this->getMockBuilder(IUserManager::class)->getMock();
@@ -165,7 +140,10 @@ private function getInstance(array $mockedMethods = []) {
165140
}
166141

167142
protected function tearDown(): void {
168-
$this->connection->getQueryBuilder()->delete('share')->execute();
143+
$this->connection
144+
->getQueryBuilder()
145+
->delete('share')
146+
->executeStatement();
169147

170148
parent::tearDown();
171149
}
@@ -305,7 +283,11 @@ public function testCreateSendPasswordByMailWithPasswordAndWithoutEnforcedPasswo
305283
// Assume the mail address is valid.
306284
$this->mailer->expects($this->any())->method('validateMailAddress')->willReturn(true);
307285

308-
$instance = $this->getInstance(['getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject', 'createShareActivity', 'autoGeneratePassword', 'createPasswordSendActivity', 'sendEmail', 'sendPassword', 'sendPasswordToOwner']);
286+
$instance = $this->getInstance([
287+
'getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject',
288+
'createShareActivity', 'autoGeneratePassword', 'createPasswordSendActivity',
289+
'sendEmail', 'sendPassword', 'sendPasswordToOwner',
290+
]);
309291

310292
$instance->expects($this->once())->method('getSharedWith')->willReturn([]);
311293
$instance->expects($this->once())->method('createMailShare')->with($share)->willReturn(42);
@@ -361,7 +343,7 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPe
361343
->willReturn('autogeneratedPassword');
362344
$this->eventDispatcher->expects($this->once())
363345
->method('dispatchTyped')
364-
->with(new GenerateSecurePasswordEvent());
346+
->with(new GenerateSecurePasswordEvent(PasswordContext::SHARING));
365347

366348
// Assume the mail address is valid.
367349
$this->mailer->expects($this->any())->method('validateMailAddress')->willReturn(true);
@@ -822,7 +804,7 @@ public function dataUpdateSendPassword() {
822804
* @param bool sendMail
823805
*/
824806
public function testUpdateSendPassword($plainTextPassword, string $originalPassword, string $newPassword, $originalSendPasswordByTalk, $newSendPasswordByTalk, bool $sendMail) {
825-
$node = $this->getMockBuilder(File::class)->getMock();
807+
$node = $this->createMock(File::class);
826808
$node->expects($this->any())->method('getName')->willReturn('filename');
827809

828810
$this->settingsManager->method('sendPasswordByMail')->willReturn(true);
@@ -927,7 +909,7 @@ public function testGetShareByPath() {
927909
$permissions = 1;
928910
$token = 'token';
929911

930-
$node = $this->getMockBuilder('OCP\Files\Node')->getMock();
912+
$node = $this->createMock(Node::class);
931913
$node->expects($this->once())->method('getId')->willReturn($itemSource);
932914

933915

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@
678678
'OCP\\Security\\Ip\\IFactory' => $baseDir . '/lib/public/Security/Ip/IFactory.php',
679679
'OCP\\Security\\Ip\\IRange' => $baseDir . '/lib/public/Security/Ip/IRange.php',
680680
'OCP\\Security\\Ip\\IRemoteAddress' => $baseDir . '/lib/public/Security/Ip/IRemoteAddress.php',
681+
'OCP\\Security\\PasswordContext' => $baseDir . '/lib/public/Security/PasswordContext.php',
681682
'OCP\\Security\\RateLimiting\\ILimiter' => $baseDir . '/lib/public/Security/RateLimiting/ILimiter.php',
682683
'OCP\\Security\\RateLimiting\\IRateLimitExceededException' => $baseDir . '/lib/public/Security/RateLimiting/IRateLimitExceededException.php',
683684
'OCP\\Security\\VerificationToken\\IVerificationToken' => $baseDir . '/lib/public/Security/VerificationToken/IVerificationToken.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
711711
'OCP\\Security\\Ip\\IFactory' => __DIR__ . '/../../..' . '/lib/public/Security/Ip/IFactory.php',
712712
'OCP\\Security\\Ip\\IRange' => __DIR__ . '/../../..' . '/lib/public/Security/Ip/IRange.php',
713713
'OCP\\Security\\Ip\\IRemoteAddress' => __DIR__ . '/../../..' . '/lib/public/Security/Ip/IRemoteAddress.php',
714+
'OCP\\Security\\PasswordContext' => __DIR__ . '/../../..' . '/lib/public/Security/PasswordContext.php',
714715
'OCP\\Security\\RateLimiting\\ILimiter' => __DIR__ . '/../../..' . '/lib/public/Security/RateLimiting/ILimiter.php',
715716
'OCP\\Security\\RateLimiting\\IRateLimitExceededException' => __DIR__ . '/../../..' . '/lib/public/Security/RateLimiting/IRateLimitExceededException.php',
716717
'OCP\\Security\\VerificationToken\\IVerificationToken' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/IVerificationToken.php',

lib/public/Security/Events/GenerateSecurePasswordEvent.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,55 @@
99
namespace OCP\Security\Events;
1010

1111
use OCP\EventDispatcher\Event;
12+
use OCP\Security\PasswordContext;
1213

1314
/**
15+
* Event to request a secure password to be generated
1416
* @since 18.0.0
1517
*/
1618
class GenerateSecurePasswordEvent extends Event {
17-
/** @var null|string */
18-
private $password;
19+
private ?string $password;
1920

2021
/**
22+
* Request a secure password to be generated.
23+
*
24+
* By default passwords are generated for the user account context,
25+
* this can be adjusted by passing another `PasswordContext`.
26+
* @since 31.0.0
27+
*/
28+
public function __construct(
29+
private PasswordContext $context = PasswordContext::ACCOUNT,
30+
) {
31+
parent::__construct();
32+
$this->password = null;
33+
}
34+
35+
/**
36+
* Get the generated password.
37+
*
38+
* If a password generator is registered and successfully generated a password
39+
* that password can get read back. Otherwise `null` is returned.
2140
* @since 18.0.0
2241
*/
2342
public function getPassword(): ?string {
2443
return $this->password;
2544
}
2645

2746
/**
47+
* Set the generated password.
48+
*
49+
* This is used by password generators to set the generated password.
2850
* @since 18.0.0
2951
*/
3052
public function setPassword(string $password): void {
3153
$this->password = $password;
3254
}
55+
56+
/**
57+
* Get the context this password should generated for.
58+
* @since 31.0.0
59+
*/
60+
public function getContext(): PasswordContext {
61+
return $this->context;
62+
}
3363
}

lib/public/Security/Events/ValidatePasswordPolicyEvent.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,41 @@
99
namespace OCP\Security\Events;
1010

1111
use OCP\EventDispatcher\Event;
12+
use OCP\Security\PasswordContext;
1213

1314
/**
15+
* This event can be emitted to request a validation of a password.
16+
*
17+
* If a password policy app is installed and the password
18+
* is invalid, an `\OCP\HintException` will be thrown.
1419
* @since 18.0.0
1520
*/
1621
class ValidatePasswordPolicyEvent extends Event {
17-
/** @var string */
18-
private $password;
1922

2023
/**
2124
* @since 18.0.0
25+
* @since 31.0.0 - $context parameter added
2226
*/
23-
public function __construct(string $password) {
27+
public function __construct(
28+
private string $password,
29+
private PasswordContext $context = PasswordContext::ACCOUNT,
30+
) {
2431
parent::__construct();
25-
$this->password = $password;
2632
}
2733

2834
/**
35+
* Get the password that should be validated.
2936
* @since 18.0.0
3037
*/
3138
public function getPassword(): string {
3239
return $this->password;
3340
}
41+
42+
/**
43+
* Get the context this password should validated for.
44+
* @since 31.0.0
45+
*/
46+
public function getContext(): PasswordContext {
47+
return $this->context;
48+
}
3449
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCP\Security;
9+
10+
/**
11+
* Define the context in which a password is used.
12+
* This allows setting a context for password validation and password generation.
13+
*
14+
* @package OCP\Security
15+
* @since 31.0.0
16+
*/
17+
enum PasswordContext {
18+
/**
19+
* Password used for an user account
20+
* @since 31.0.0
21+
*/
22+
case ACCOUNT;
23+
24+
/**
25+
* Password used for (public) shares
26+
* @since 31.0.0
27+
*/
28+
case SHARING;
29+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace Test\Security\Events;
11+
12+
use OCP\Security\Events\GenerateSecurePasswordEvent;
13+
use OCP\Security\PasswordContext;
14+
15+
class GenerateSecurePasswordEventTest extends \Test\TestCase {
16+
17+
public function testDefaultProperties() {
18+
$event = new GenerateSecurePasswordEvent();
19+
$this->assertNull($event->getPassword());
20+
$this->assertEquals(PasswordContext::ACCOUNT, $event->getContext());
21+
}
22+
23+
public function testSettingPassword() {
24+
$event = new GenerateSecurePasswordEvent();
25+
$event->setPassword('example');
26+
$this->assertEquals('example', $event->getPassword());
27+
}
28+
29+
public function testSettingContext() {
30+
$event = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
31+
$this->assertEquals(PasswordContext::SHARING, $event->getContext());
32+
}
33+
}

0 commit comments

Comments
 (0)