Adds a "Request password" button to the public share authentication page for shares

of type TYPE_EMAIL, when the "video verification" checkbox isn't checked. Users accessing
non-anonymous public shares (TYPE_EMAIL shares) can now request a temporary password themselves.

- Creates a migration step for the files_sharing app to add the 'password_expiration_time'
  attribute to the oc_shares table.
- Makes share temporary passwords' expiration time configurable via a system value.
- Adds a system config value to allow permanent share passwords

-Fixes a typo in a comment in apps/files_sharing/src/components/SharingEntryLink.vue

See https://github.com/nextcloud/server/issues/31005

Signed-off-by: Cyrille Bollu <cyrpub@bollu.be>
pull/31220/head
Cyrille Bollu 2 years ago
parent 60f946aba5
commit c6a5c07041

@ -9,7 +9,7 @@
Turning the feature off removes shared files and folders on the server for all share recipients, and also on the sync clients and mobile apps. More information is available in the Nextcloud Documentation.
</description>
<version>1.16.1</version>
<version>1.16.2</version>
<licence>agpl</licence>
<author>Michael Gapczynski</author>
<author>Bjoern Schiessle</author>

@ -68,6 +68,7 @@ return array(
'OCA\\Files_Sharing\\Migration\\Version11300Date20201120141438' => $baseDir . '/../lib/Migration/Version11300Date20201120141438.php',
'OCA\\Files_Sharing\\Migration\\Version21000Date20201223143245' => $baseDir . '/../lib/Migration/Version21000Date20201223143245.php',
'OCA\\Files_Sharing\\Migration\\Version22000Date20210216084241' => $baseDir . '/../lib/Migration/Version22000Date20210216084241.php',
'OCA\\Files_Sharing\\Migration\\Version24000Date20220208195521' => $baseDir . '/../lib/Migration/Version24000Date20220208195521.php',
'OCA\\Files_Sharing\\Migration\\Version24000Date20220404142216' => $baseDir . '/../lib/Migration/Version24000Date20220404142216.php',
'OCA\\Files_Sharing\\MountProvider' => $baseDir . '/../lib/MountProvider.php',
'OCA\\Files_Sharing\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php',

@ -83,6 +83,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Migration\\Version11300Date20201120141438' => __DIR__ . '/..' . '/../lib/Migration/Version11300Date20201120141438.php',
'OCA\\Files_Sharing\\Migration\\Version21000Date20201223143245' => __DIR__ . '/..' . '/../lib/Migration/Version21000Date20201223143245.php',
'OCA\\Files_Sharing\\Migration\\Version22000Date20210216084241' => __DIR__ . '/..' . '/../lib/Migration/Version22000Date20210216084241.php',
'OCA\\Files_Sharing\\Migration\\Version24000Date20220208195521' => __DIR__ . '/..' . '/../lib/Migration/Version24000Date20220208195521.php',
'OCA\\Files_Sharing\\Migration\\Version24000Date20220404142216' => __DIR__ . '/..' . '/../lib/Migration/Version24000Date20220404142216.php',
'OCA\\Files_Sharing\\MountProvider' => __DIR__ . '/..' . '/../lib/MountProvider.php',
'OCA\\Files_Sharing\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php',

@ -5,7 +5,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'reference' => 'c6429e6cd19c57582364338362e543580821cf99',
'reference' => 'ea4531aaaa6eb9fb3859e05b69ab773bfbfe7437',
'name' => '__root__',
'dev' => false,
),
@ -16,7 +16,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'reference' => 'c6429e6cd19c57582364338362e543580821cf99',
'reference' => 'ea4531aaaa6eb9fb3859e05b69ab773bfbfe7437',
'dev_requirement' => false,
),
),

@ -279,6 +279,7 @@ class ShareAPIController extends OCSController {
} elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
$result['share_with'] = $share->getSharedWith();
$result['password'] = $share->getPassword();
$result['password_expiration_time'] = $share->getPasswordExpirationTime();
$result['send_password_by_talk'] = $share->getSendPasswordByTalk();
$result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL');
$result['token'] = $share->getToken();
@ -570,6 +571,10 @@ class ShareAPIController extends OCSController {
// Set password
if ($password !== '') {
$share->setPassword($password);
// Shares shared by email have temporary passwords by default
if ($shareType === IShare::TYPE_EMAIL) {
$this->setSharePasswordExpirationTime($share);
}
}
// Only share by mail have a recipient
@ -1177,6 +1182,9 @@ class ShareAPIController extends OCSController {
$share->setPassword(null);
} elseif ($password !== null) {
$share->setPassword($password);
if ($share->getShareType() === IShare::TYPE_EMAIL) {
$this->setSharePasswordExpirationTime($share);
}
}
if ($label !== null) {
@ -1513,6 +1521,35 @@ class ShareAPIController extends OCSController {
return $date;
}
/**
* Set the share's password expiration time
*/
private function setSharePasswordExpirationTime(IShare $share): void {
if ($this->config->getSystemValue('allow_mail_share_permanent_password')) {
// Sets password expiration date to NULL
$share->setPasswordExpirationTime();
return;
}
// Sets password expiration date
$expirationTime = null;
try {
$now = new \DateTime();
$expirationInterval = $this->config->getSystemValue('share_temporary_password_expiration_interval');
if ($expirationInterval === '' || is_null($expirationInterval)) {
$expirationInterval = 'P0DT15M';
}
$expirationTime = $now->add(new \DateInterval($expirationInterval));
} catch (\Exception $e) {
// Catches invalid format for system value 'share_temporary_password_expiration_interval'
\OC::$server->getLogger()->logException($e, [
'message' => 'The \'share_temporary_password_expiration_interval\' system setting does not respect the DateInterval::__construct() format. Setting it to \'P0DT15M\''
]);
$expirationTime = $now->add(new \DateInterval('P0DT15M'));
} finally {
$share->setPasswordExpirationTime($expirationTime);
}
}
/**
* Since we have multiple providers but the OCS Share API v1 does
* not support this we need to check all backends.

@ -72,6 +72,7 @@ use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
use OCP\Share;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager as ShareManager;
@ -84,53 +85,21 @@ use OCP\Template;
* @package OCA\Files_Sharing\Controllers
*/
class ShareController extends AuthPublicShareController {
protected IConfig $config;
protected IUserManager $userManager;
protected ILogger $logger;
protected \OCP\Activity\IManager $activityManager;
protected IPreview $previewManager;
protected IRootFolder $rootFolder;
protected FederatedShareProvider $federatedShareProvider;
protected IAccountManager $accountManager;
protected IEventDispatcher $eventDispatcher;
protected IL10N $l10n;
protected Defaults $defaults;
protected ShareManager $shareManager;
protected ISecureRandom $secureRandom;
protected ?Share\IShare $share = null;
/** @var IConfig */
protected $config;
/** @var IUserManager */
protected $userManager;
/** @var ILogger */
protected $logger;
/** @var \OCP\Activity\IManager */
protected $activityManager;
/** @var IPreview */
protected $previewManager;
/** @var IRootFolder */
protected $rootFolder;
/** @var FederatedShareProvider */
protected $federatedShareProvider;
/** @var IAccountManager */
protected $accountManager;
/** @var IEventDispatcher */
protected $eventDispatcher;
/** @var IL10N */
protected $l10n;
/** @var Defaults */
protected $defaults;
/** @var ShareManager */
protected $shareManager;
/** @var Share\IShare */
protected $share;
/**
* @param string $appName
* @param IRequest $request
* @param IConfig $config
* @param IURLGenerator $urlGenerator
* @param IUserManager $userManager
* @param ILogger $logger
* @param \OCP\Activity\IManager $activityManager
* @param \OCP\Share\IManager $shareManager
* @param ISession $session
* @param IPreview $previewManager
* @param IRootFolder $rootFolder
* @param FederatedShareProvider $federatedShareProvider
* @param IAccountManager $accountManager
* @param IEventDispatcher $eventDispatcher
* @param IL10N $l10n
* @param Defaults $defaults
*/
public function __construct(string $appName,
IRequest $request,
IConfig $config,
@ -146,6 +115,7 @@ class ShareController extends AuthPublicShareController {
IAccountManager $accountManager,
IEventDispatcher $eventDispatcher,
IL10N $l10n,
ISecureRandom $secureRandom,
Defaults $defaults) {
parent::__construct($appName, $request, $session, $urlGenerator);
@ -159,6 +129,7 @@ class ShareController extends AuthPublicShareController {
$this->accountManager = $accountManager;
$this->eventDispatcher = $eventDispatcher;
$this->l10n = $l10n;
$this->secureRandom = $secureRandom;
$this->defaults = $defaults;
$this->shareManager = $shareManager;
}
@ -209,6 +180,56 @@ class ShareController extends AuthPublicShareController {
return $response;
}
/**
* The template to show after user identification
*/
protected function showIdentificationResult(bool $success = false): TemplateResponse {
$templateParameters = ['share' => $this->share, 'identityOk' => $success];
$this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
$response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
if ($this->share->getSendPasswordByTalk()) {
$csp = new ContentSecurityPolicy();
$csp->addAllowedConnectDomain('*');
$csp->addAllowedMediaDomain('blob:');
$response->setContentSecurityPolicy($csp);
}
return $response;
}
/**
* Validate the identity token of a public share
*
* @param ?string $identityToken
* @return bool
*/
protected function validateIdentity(?string $identityToken = null): bool {
if ($this->share->getShareType() !== IShare::TYPE_EMAIL) {
return false;
}
if ($identityToken === null || $this->share->getSharedWith() === null) {
return false;
}
return $identityToken === $this->share->getSharedWith();
}
/**
* Generates a password for the share, respecting any password policy defined
*/
protected function generatePassword(): void {
$event = new \OCP\Security\Events\GenerateSecurePasswordEvent();
$this->eventDispatcher->dispatchTyped($event);
$password = $event->getPassword() ?? $this->secureRandom->generate(20);
$this->share->setPassword($password);
$this->shareManager->updateShare($this->share);
}
protected function verifyPassword(string $password): bool {
return $this->shareManager->checkPassword($this->share, $password);
}

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Vincent Petry <vincent@nextloud.com>
*
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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 OCA\Files_Sharing\Migration;
use Closure;
use OCP\DB\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version24000Date20220208195521 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();
$table = $schema->getTable('share');
$table->addColumn('password_expiration_time', Types::DATETIME, [
'notnull' => false,
]);
return $schema;
}
}

@ -780,8 +780,8 @@ export default {
/**
* Uncheck password protection
* We need this method because @update:checked
* is ran simultaneously as @uncheck, so
* so we cannot ensure data is up-to-date
* is ran simultaneously as @uncheck, so we
* cannot ensure data is up-to-date
*/
onPasswordDisable() {
this.share.password = ''

@ -4410,6 +4410,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'password_expiration_time' => null,
], $share, [], false
];
@ -4459,6 +4460,7 @@ class ShareAPIControllerTest extends TestCase {
'hide_download' => 0,
'can_edit' => false,
'can_delete' => false,
'password_expiration_time' => null,
], $share, [], false
];

@ -106,6 +106,8 @@ class ShareControllerTest extends \Test\TestCase {
private $eventDispatcher;
/** @var IL10N */
private $l10n;
/** @var ISecureRandom */
private $secureRandom;
/** @var Defaults|MockObject */
private $defaults;
@ -127,6 +129,7 @@ class ShareControllerTest extends \Test\TestCase {
$this->accountManager = $this->createMock(IAccountManager::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->l10n = $this->createMock(IL10N::class);
$this->secureRandom = $this->createMock(ISecureRandom::class);
$this->defaults = $this->createMock(Defaults::class);
$this->shareController = new \OCA\Files_Sharing\Controller\ShareController(
@ -145,6 +148,7 @@ class ShareControllerTest extends \Test\TestCase {
$this->accountManager,
$this->eventDispatcher,
$this->l10n,
$this->secureRandom,
$this->defaults
);

@ -52,6 +52,7 @@ use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\HintException;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
@ -75,6 +76,8 @@ use OCP\Share\IShareProvider;
*/
class ShareByMailProvider implements IShareProvider {
private IConfig $config;
/** @var IDBConnection */
private $dbConnection;
@ -126,7 +129,8 @@ class ShareByMailProvider implements IShareProvider {
return 'ocMailShare';
}
public function __construct(IDBConnection $connection,
public function __construct(IConfig $config,
IDBConnection $connection,
ISecureRandom $secureRandom,
IUserManager $userManager,
IRootFolder $rootFolder,
@ -140,6 +144,7 @@ class ShareByMailProvider implements IShareProvider {
IHasher $hasher,
IEventDispatcher $eventDispatcher,
IShareManager $shareManager) {
$this->config = $config;
$this->dbConnection = $connection;
$this->secureRandom = $secureRandom;
$this->userManager = $userManager;
@ -190,9 +195,14 @@ class ShareByMailProvider implements IShareProvider {
}
$shareId = $this->createMailShare($share);
$send = $this->sendPassword($share, $password);
if ($passwordEnforced && $send === false) {
$this->sendPasswordToOwner($share, $password);
// Sends share password to receiver when it's a permanent one (otherwise she will have to request it via the showShare UI)
// or to owner when the password shall be given during a Talk session
if ($this->config->getSystemValue('allow_mail_share_permanent_password') || $share->getSendPasswordByTalk()) {
$send = $this->sendPassword($share, $password);
if ($passwordEnforced && $send === false) {
$this->sendPasswordToOwner($share, $password);
}
}
$this->createShareActivity($share);
@ -327,6 +337,7 @@ class ShareByMailProvider implements IShareProvider {
$share->getPermissions(),
$share->getToken(),
$share->getPassword(),
$share->getPasswordExpirationTime(),
$share->getSendPasswordByTalk(),
$share->getHideDownload(),
$share->getLabel(),
@ -673,23 +684,24 @@ class ShareByMailProvider implements IShareProvider {
}
/**
* add share to the database and return the ID
*
* @param int $itemSource
* @param string $itemType
* @param string $shareWith
* @param string $sharedBy
* @param string $uidOwner
* @param int $permissions
* @param string $token
* @param string $password
* @param bool $sendPasswordByTalk
* @param bool $hideDownload
* @param string $label
* @param \DateTime|null $expirationTime
* @return int
* Add share to the database and return the ID
*/
protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password, $sendPasswordByTalk, $hideDownload, $label, $expirationTime, $note = ''): int {
protected function addShareToDB(
?int $itemSource,
?string $itemType,
?string $shareWith,
?string $sharedBy,
?string $uidOwner,
?int $permissions,
?string $token,
?string $password,
?\DateTimeInterface $passwordExpirationTime,
?bool $sendPasswordByTalk,
?bool $hideDownload,
?string $label,
?\DateTimeInterface $expirationTime,
?string $note = ''
): int {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert('share')
->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))
@ -702,6 +714,7 @@ class ShareByMailProvider implements IShareProvider {
->setValue('permissions', $qb->createNamedParameter($permissions))
->setValue('token', $qb->createNamedParameter($token))
->setValue('password', $qb->createNamedParameter($password))
->setValue('password_expiration_time', $qb->createNamedParameter($passwordExpirationTime, IQueryBuilder::PARAM_DATE))
->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL))
->setValue('stime', $qb->createNamedParameter(time()))
->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT))
@ -739,6 +752,7 @@ class ShareByMailProvider implements IShareProvider {
($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()))) {
$this->sendPassword($share, $plainTextPassword);
}
/*
* We allow updating the permissions and password of mail shares
*/
@ -749,6 +763,7 @@ class ShareByMailProvider implements IShareProvider {
->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
->set('password', $qb->createNamedParameter($share->getPassword()))
->set('password_expiration_time', $qb->createNamedParameter($share->getPasswordExpirationTime(), IQueryBuilder::PARAM_DATE))
->set('label', $qb->createNamedParameter($share->getLabel()))
->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL))
->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
@ -1012,6 +1027,8 @@ class ShareByMailProvider implements IShareProvider {
$share->setShareTime($shareTime);
$share->setSharedWith($data['share_with']);
$share->setPassword($data['password']);
$passwordExpirationTime = \DateTime::createFromFormat('Y-m-d H:i:s', $data['password_expiration_time']);
$share->setPasswordExpirationTime($passwordExpirationTime !== false? $passwordExpirationTime : null);
$share->setLabel($data['label']);
$share->setSendPasswordByTalk((bool)$data['password_by_talk']);
$share->setHideDownload((bool)$data['hide_download']);

@ -39,6 +39,7 @@ use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
@ -63,6 +64,9 @@ use Test\TestCase;
*/
class ShareByMailProviderTest extends TestCase {
/** @var IConfig */
private $config;
/** @var IDBConnection */
private $connection;
@ -111,6 +115,7 @@ class ShareByMailProviderTest extends TestCase {
protected function setUp(): void {
parent::setUp();
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
$this->connection = \OC::$server->getDatabaseConnection();
$this->l = $this->getMockBuilder(IL10N::class)->getMock();
@ -145,6 +150,7 @@ class ShareByMailProviderTest extends TestCase {
$instance = $this->getMockBuilder('OCA\ShareByMail\ShareByMailProvider')
->setConstructorArgs(
[
$this->config,
$this->connection,
$this->secureRandom,
$this->userManager,
@ -168,6 +174,7 @@ class ShareByMailProviderTest extends TestCase {
}
return new ShareByMailProvider(
$this->config,
$this->connection,
$this->secureRandom,
$this->userManager,
@ -267,9 +274,43 @@ class ShareByMailProviderTest extends TestCase {
$this->hasher->expects($this->once())->method('hash')->with('password')->willReturn('passwordHashed');
$share->expects($this->once())->method('setPassword')->with('passwordHashed');
// The given password (but not the autogenerated password) should not be
// mailed to the receiver of the share because permanent passwords are not enforced.
$this->shareManager->expects($this->any())->method('shareApiLinkEnforcePassword')->willReturn(false);
$this->config->expects($this->once())->method('getSystemValue')->with('allow_mail_share_permanent_password')->willReturn(false);
$instance->expects($this->never())->method('autoGeneratePassword');
$this->assertSame('shareObject',
$instance->create($share)
);
}
public function testCreateSendPasswordByMailWithPasswordAndWithoutEnforcedPasswordProtectionWithPermanentPassword() {
$share = $this->getMockBuilder(IShare::class)->getMock();
$share->expects($this->any())->method('getSharedWith')->willReturn('receiver@example.com');
$share->expects($this->any())->method('getSendPasswordByTalk')->willReturn(false);
$share->expects($this->any())->method('getSharedBy')->willReturn('owner');
$node = $this->getMockBuilder(File::class)->getMock();
$node->expects($this->any())->method('getName')->willReturn('filename');
$instance = $this->getInstance(['getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject', 'createShareActivity', 'autoGeneratePassword', 'createPasswordSendActivity']);
$instance->expects($this->once())->method('getSharedWith')->willReturn([]);
$instance->expects($this->once())->method('createMailShare')->with($share)->willReturn(42);
$instance->expects($this->once())->method('createShareActivity')->with($share);
$instance->expects($this->once())->method('getRawShare')->with(42)->willReturn('rawShare');
$instance->expects($this->once())->method('createShareObject')->with('rawShare')->willReturn('shareObject');
$share->expects($this->any())->method('getNode')->willReturn($node);
$share->expects($this->once())->method('getPassword')->willReturn('password');
$this->hasher->expects($this->once())->method('hash')->with('password')->willReturn('passwordHashed');
$share->expects($this->once())->method('setPassword')->with('passwordHashed');
// The given password (but not the autogenerated password) should be
// mailed to the receiver of the share.
// mailed to the receiver of the share because permanent passwords are enforced.
$this->shareManager->expects($this->any())->method('shareApiLinkEnforcePassword')->willReturn(false);
$this->config->expects($this->once())->method('getSystemValue')->with('allow_mail_share_permanent_password')->willReturn(true);
$this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true);
$instance->expects($this->never())->method('autoGeneratePassword');
@ -290,7 +331,7 @@ class ShareByMailProviderTest extends TestCase {
);
}
public function testCreateSendPasswordByMailWithEnforcedPasswordProtection() {
public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPermanentPassword() {
$share = $this->getMockBuilder(IShare::class)->getMock();
$share->expects($this->any())->method('getSharedWith')->willReturn('receiver@example.com');
$share->expects($this->any())->method('getSendPasswordByTalk')->willReturn(false);
@ -320,8 +361,9 @@ class ShareByMailProviderTest extends TestCase {
$this->hasher->expects($this->once())->method('hash')->with('autogeneratedPassword')->willReturn('autogeneratedPasswordHashed');
$share->expects($this->once())->method('setPassword')->with('autogeneratedPasswordHashed');
// The autogenerated password should be mailed to the receiver of the share.
// The autogenerated password should be mailed to the receiver of the share because permanent passwords are enforced.
$this->shareManager->expects($this->any())->method('shareApiLinkEnforcePassword')->willReturn(true);
$this->config->expects($this->once())->method('getSystemValue')->with('allow_mail_share_permanent_password')->willReturn(true);
$this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true);
$message = $this->createMock(IMessage::class);
@ -341,7 +383,7 @@ class ShareByMailProviderTest extends TestCase {
);
}
public function testCreateSendPasswordByMailWithPasswordAndWithEnforcedPasswordProtection() {
public function testCreateSendPasswordByMailWithPasswordAndWithEnforcedPasswordProtectionWithPermanentPassword() {
$share = $this->getMockBuilder(IShare::class)->getMock();
$share->expects($this->any())->method('getSharedWith')->willReturn('receiver@example.com');
$share->expects($this->any())->method('getSendPasswordByTalk')->willReturn(false);
@ -366,6 +408,7 @@ class ShareByMailProviderTest extends TestCase {
// The given password (but not the autogenerated password) should be
// mailed to the receiver of the share.
$this->shareManager->expects($this->any())->method('shareApiLinkEnforcePassword')->willReturn(true);
$this->config->expects($this->once())->method('getSystemValue')->with('allow_mail_share_permanent_password')->willReturn(true);
$this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true);
$instance->expects($this->never())->method('autoGeneratePassword');
@ -410,6 +453,7 @@ class ShareByMailProviderTest extends TestCase {
// The autogenerated password should be mailed to the owner of the share.
$this->shareManager->expects($this->any())->method('shareApiLinkEnforcePassword')->willReturn(true);
$this->config->expects($this->once())->method('getSystemValue')->with('allow_mail_share_permanent_password')->willReturn(false);
$this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true);
$instance->expects($this->once())->method('autoGeneratePassword')->with($share)->willReturn('autogeneratedPassword');
@ -531,6 +575,7 @@ class ShareByMailProviderTest extends TestCase {
$hideDownload = true;
$label = 'label';
$expiration = new \DateTime();
$passwordExpirationTime = new \DateTime();
$instance = $this->getInstance();
@ -546,6 +591,7 @@ class ShareByMailProviderTest extends TestCase {
$permissions,
$token,
$password,
$passwordExpirationTime,
$sendPasswordByTalk,
$hideDownload,
$label,
@ -572,6 +618,7 @@ class ShareByMailProviderTest extends TestCase {
$this->assertSame($permissions, (int)$result[0]['permissions']);
$this->assertSame($token, $result[0]['token']);
$this->assertSame($password, $result[0]['password']);
$this->assertSame($passwordExpirationTime->getTimestamp(), \DateTime::createFromFormat('Y-m-d H:i:s', $result[0]['password_expiration_time'])->getTimestamp());
$this->assertSame($sendPasswordByTalk, (bool)$result[0]['password_by_talk']);
$this->assertSame($hideDownload, (bool)$result[0]['hide_download']);
$this->assertSame($label, $result[0]['label']);

@ -7,6 +7,7 @@ form fieldset > p {
position: relative;
}
#email,
#password {
margin: 5px 0;
padding-right: 45px;
@ -17,8 +18,10 @@ form fieldset > p {
min-width: 0; /* FF hack for to override default value */
}
input[type='submit'],
input[type='submit'].icon-confirm {
#password-input-form input[type='submit'],
#email-input-form input[type='submit'],
#email-input-form input[type='submit'].icon-confirm,
#password-input-form input[type='submit'].icon-confirm {
position: absolute;
top: 0px;
right: -5px;

@ -1,11 +1,52 @@
function showEmailAddressPromptForm() {
// Shows email prompt
var emailInput = document.getElementById('email-input-form');
emailInput.style.display="block";
// Shows back button
var backButton = document.getElementById('request-password-back-button');
backButton.style.display="block";
// Hides password prompt and 'request password' button
var passwordRequestButton = document.getElementById('request-password-button-not-talk');
var passwordInput = document.getElementById('password-input-form');
passwordRequestButton.style.display="none";
passwordInput.style.display="none";
// Hides identification result messages, if any
var identificationResultSuccess = document.getElementById('identification-success');
var identificationResultFailure = document.getElementById('identification-failure');
if (identificationResultSuccess) {
identificationResultSuccess.style.display="none";
}
if (identificationResultFailure) {
identificationResultFailure.style.display="none";
}
}
document.addEventListener('DOMContentLoaded', function() {
// Enables password submit button only when user has typed something in the password field
var passwordInput = document.getElementById('password');
var passwordButton = document.getElementById('password-submit');
var eventListener = function() {
passwordButton.disabled = passwordInput.value.length === 0;
};
passwordInput.addEventListener('click', eventListener);
passwordInput.addEventListener('keyup', eventListener);
passwordInput.addEventListener('change', eventListener);
// Enables email request button only when user has typed something in the email field
var emailInput = document.getElementById('email');
var emailButton = document.getElementById('password-request');
eventListener = function() {
emailButton.disabled = emailInput.value.length === 0;
};
emailInput.addEventListener('click', eventListener);
emailInput.addEventListener('keyup', eventListener);
emailInput.addEventListener('change', eventListener);
// Adds functionality to the request password button
var passwordRequestButton = document.getElementById('request-password-button-not-talk');
passwordRequestButton.addEventListener('click', showEmailAddressPromptForm);
});

@ -5,7 +5,13 @@
style('core', 'publicshareauth');
script('core', 'publicshareauth');
?>
<form method="post">
<!-- password prompt form. It should be hidden when we show the email prompt form -->
<?php if (!isset($_['identityOk'])): ?>
<form method="post" id="password-input-form">
<?php else: ?>
<form method="post" id="password-input-form" style="display:none;">
<?php endif; ?>
<fieldset class="warning">
<?php if (!isset($_['wrongpw'])): ?>
<div class="warning-info"><?php p($l->t('This share is password-protected')); ?></div>
@ -21,8 +27,57 @@
autocomplete="new-password" autocapitalize="off" autocorrect="off"
autofocus />
<input type="hidden" name="sharingToken" value="<?php p($_['share']->getToken()) ?>" id="sharingToken">
<input type="submit" id="password-submit"
<input type="hidden" name="sharingType" value="<?php p($_['share']->getShareType()) ?>" id="sharingType">
<input type="submit" id="password-submit"
class="svg icon-confirm input-button-inline" value="" disabled="disabled" />
</p>
</fieldset>
</form>
<!-- email prompt form. It should initially be hidden -->
<?php if (isset($_['identityOk'])): ?>
<form method="post" id="email-input-form">
<?php else: ?>
<form method="post" id="email-input-form" style="display:none;">
<?php endif; ?>
<fieldset class="warning">
<div class="warning-info" id="email-prompt"><?php p($l->t('Please type in your email address to request a temporary password')); ?></div>
<p>
<input type="email" id="email" name="identityToken" placeholder="<?php p($l->t('Email address')); ?>" />
<input type="submit" id="password-request" name="passwordRequest" class="svg icon-confirm input-button-inline" value="" disabled="disabled"/>
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
<input type="hidden" name="sharingToken" value="<?php p($_['share']->getToken()) ?>" id="sharingToken">
<input type="hidden" name="sharingType" value="<?php p($_['share']->getShareType()) ?>" id="sharingType">
</p>
<?php if (isset($_['identityOk'])): ?>
<?php if ($_['identityOk']): ?>
<div class="warning-info" id="identification-success"><?php p($l->t('Password sent!')); ?></div>
<?php else: ?>
<div class="warning" id="identification-failure"><?php p($l->t('You are not authorized to request a password for this share')); ?></div>
<?php endif; ?>
<?php endif; ?>
</fieldset>
</form>
<!-- request password button -->
<?php if (!isset($_['identityOk']) && $_['share']->getShareType() === $_['share']::TYPE_EMAIL && !$_['share']->getSendPasswordByTalk()): ?>
<input type="button"
id="request-password-button-not-talk"
value="<?php p($l->t('Request password')); ?>"
class="primary" />
<?php endif; ?>
<!-- back to showShare button -->
<form method="get">
<fieldset>
<input type="submit"
id="request-password-back-button"
value="<?php p($l->t('Back')); ?>"
class="primary"
<?php if (isset($_['identityOk'])): ?>
style="display:block;" />
<?php else: ?>
style="display:none;" />
<?php endif; ?>
</fieldset>
</form>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1552,6 +1552,12 @@ class Manager implements IManager {
return false;
}
// Makes sure password hasn't expired
$expirationTime = $share->getPasswordExpirationTime();
if ($expirationTime !== null && $expirationTime < new \DateTime()) {
return false;
}
$newHash = '';
if (!$this->hasher->verify($password, $share->getPassword(), $newHash)) {
return false;

@ -185,6 +185,7 @@ class ProviderFactory implements IProviderFactory {
$settingsManager = new SettingsManager($this->serverContainer->getConfig());
$this->shareByMailProvider = new ShareByMailProvider(
$this->serverContainer->getConfig(),
$this->serverContainer->getDatabaseConnection(),
$this->serverContainer->getSecureRandom(),
$this->serverContainer->getUserManager(),

@ -73,6 +73,7 @@ class Share implements IShare {
private $expireDate;
/** @var string */
private $password;
private ?\DateTimeInterface $passwordExpirationTime = null;
/** @var bool */
private $sendPasswordByTalk = false;
/** @var string */
@ -461,6 +462,21 @@ class Share implements IShare {
return $this->password;
}
/**
* @inheritdoc
*/
public function setPasswordExpirationTime(?\DateTimeInterface $passwordExpirationTime = null): IShare {
$this->passwordExpirationTime = $passwordExpirationTime;
return $this;
}
/**
* @inheritdoc
*/
public function getPasswordExpirationTime(): ?\DateTimeInterface {
return $this->passwordExpirationTime;
}
/**
* @inheritdoc
*/

@ -84,12 +84,40 @@ abstract class AuthPublicShareController extends PublicShareController {
return new TemplateResponse('core', 'publicshareauth', ['wrongpw' => true], 'guest');
}
/**
* The template to show after user identification
*
* @since 24.0.0
*/
protected function showIdentificationResult(bool $success): TemplateResponse {
return new TemplateResponse('core', 'publicshareauth', ['identityOk' => $success], 'guest');
}
/**
* Validates that the provided identity is allowed to receive a temporary password
*
* @since 24.0.0
*/
protected function validateIdentity(?string $identityToken = null): bool {
return false;
}
/**
* Generates a password
*
* @since 24.0.0
*/
protected function generatePassword(): void {
}
/**
* Verify the password
*
* @since 14.0.0
* @since 24.0.0
*/
abstract protected function verifyPassword(string $password): bool;
protected function verifyPassword(string $password): bool {
return false;
}
/**
* Function called after failed authentication
@ -120,12 +148,25 @@ abstract class AuthPublicShareController extends PublicShareController {
*
* @since 14.0.0
*/
final public function authenticate(string $password = '') {
final public function authenticate(string $password = '', string $passwordRequest = 'no', string $identityToken = '') {
// Already authenticated
if ($this->isAuthenticated()) {
return $this->getRedirect();
}
// Is user requesting a temporary password?
if ($passwordRequest == '') {
if ($this->validateIdentity($identityToken)) {
$this->generatePassword();
$response = $this->showIdentificationResult(true);
return $response;
} else {
$response = $this->showIdentificationResult(false);
$response->throttle();
return $response;
}
}
if (!$this->verifyPassword($password)) {
$this->authFailed();
$response = $this->showAuthFailed();

@ -448,6 +448,19 @@ interface IShare {
*/
public function getPassword();
/**
* Set the password's expiration time of this share.
*
* @return self The modified object
* @since 24.0.0
*/
public function setPasswordExpirationTime(?\DateTimeInterface $passwordExpirationTime = null): IShare;
/**
* Get the password's expiration time of this share.
* @since 24.0.0
*/
public function getPasswordExpirationTime(): ?\DateTimeInterface;
/**
* Set if the recipient can start a conversation with the owner to get the

@ -63,7 +63,9 @@ class AuthPublicShareControllerTest extends \Test\TestCase {
'isPasswordProtected',
'isValidToken',
'showShare',
'verifyPassword'
'verifyPassword',
'validateIdentity',
'generatePassword'
])->getMock();
}

Loading…
Cancel
Save