From ff810f8e2311061908ab0f0f9f8f856a9145f0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:26:00 +0200 Subject: [PATCH 1/9] Extend mail shares unit tests to check the password and mail template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../tests/ShareByMailProviderTest.php | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php index 07bba57a19cf0..dde24138a485d 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -263,11 +263,18 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtection() { // The autogenerated password should be mailed to the receiver of the share. $this->settingsManager->expects($this->any())->method('enforcePasswordProtection')->willReturn(true); $this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true); - $instance->expects($this->once())->method('autoGeneratePassword')->with($share)->willReturn('password'); + $instance->expects($this->once())->method('autoGeneratePassword')->with($share)->willReturn('autogeneratedPassword'); $message = $this->createMock(IMessage::class); $message->expects($this->once())->method('setTo')->with(['receiver@example.com']); $this->mailer->expects($this->once())->method('createMessage')->willReturn($message); + $this->mailer->expects($this->once())->method('createEMailTemplate')->with('sharebymail.RecipientPasswordNotification', [ + 'filename' => 'filename', + 'password' => 'autogeneratedPassword', + 'initiator' => 'owner', + 'initiatorEmail' => null, + 'shareWith' => 'receiver@example.com', + ]); $this->mailer->expects($this->once())->method('send'); $this->assertSame('shareObject', @@ -296,11 +303,18 @@ public function testCreateSendPasswordByTalkWithEnforcedPasswordProtection() { // The autogenerated password should be mailed to the owner of the share. $this->settingsManager->expects($this->any())->method('enforcePasswordProtection')->willReturn(true); $this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true); - $instance->expects($this->once())->method('autoGeneratePassword')->with($share)->willReturn('password'); + $instance->expects($this->once())->method('autoGeneratePassword')->with($share)->willReturn('autogeneratedPassword'); $message = $this->createMock(IMessage::class); $message->expects($this->once())->method('setTo')->with(['owner@example.com' => 'Owner display name']); $this->mailer->expects($this->once())->method('createMessage')->willReturn($message); + $this->mailer->expects($this->once())->method('createEMailTemplate')->with('sharebymail.OwnerPasswordNotification', [ + 'filename' => 'filename', + 'password' => 'autogeneratedPassword', + 'initiator' => 'Owner display name', + 'initiatorEmail' => 'owner@example.com', + 'shareWith' => 'receiver@example.com', + ]); $this->mailer->expects($this->once())->method('send'); $user = $this->createMock(IUser::class); @@ -533,6 +547,13 @@ public function testUpdateSendPassword($plainTextPassword, string $originalPassw $share->expects($this->any())->method('getSendPasswordByTalk')->willReturn($newSendPasswordByTalk); if ($sendMail) { + $this->mailer->expects($this->once())->method('createEMailTemplate')->with('sharebymail.RecipientPasswordNotification', [ + 'filename' => 'filename', + 'password' => $plainTextPassword, + 'initiator' => null, + 'initiatorEmail' => null, + 'shareWith' => 'receiver@example.com', + ]); $this->mailer->expects($this->once())->method('send'); } else { $this->mailer->expects($this->never())->method('send'); From 149d2b00135249c1c84bcf4a134332884528e199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:27:33 +0200 Subject: [PATCH 2/9] Fix creating a mail share with a password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a mail share was created with a password the given password was not hashed, so it was not possible to open the share with that password. Moreover, if passwords were enforced the given password was ignored and a new one was set (although in this case it was hashed so it worked as expected). Now the given password is properly hashed and not overriden. Signed-off-by: Daniel Calviño Sánchez --- apps/sharebymail/lib/ShareByMailProvider.php | 10 +- .../tests/ShareByMailProviderTest.php | 98 +++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index 50338c4883015..c3636f379d82a 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -186,12 +186,16 @@ public function create(IShare $share) { // if the admin enforces a password for all mail shares we create a // random password and send it to the recipient - $password = ''; + $password = $share->getPassword() ?: ''; $passwordEnforced = $this->settingsManager->enforcePasswordProtection(); - if ($passwordEnforced) { + if ($passwordEnforced && empty($password)) { $password = $this->autoGeneratePassword($share); } + if (!empty($password)) { + $share->setPassword($this->hasher->hash($password)); + } + $shareId = $this->createMailShare($share); $send = $this->sendPassword($share, $password); if ($passwordEnforced && $send === false) { @@ -233,8 +237,6 @@ protected function autoGeneratePassword($share) { $password = $this->secureRandom->generate($passwordLength, $passwordCharset); - $share->setPassword($this->hasher->hash($password)); - return $password; } diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php index dde24138a485d..66e9f7437b6ca 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -242,6 +242,51 @@ public function testCreateSendPasswordByMailWithoutEnforcedPasswordProtection() ); } + public function testCreateSendPasswordByMailWithPasswordAndWithoutEnforcedPasswordProtection() { + $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. + $this->settingsManager->expects($this->any())->method('enforcePasswordProtection')->willReturn(false); + $this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true); + $instance->expects($this->never())->method('autoGeneratePassword'); + + $message = $this->createMock(IMessage::class); + $message->expects($this->once())->method('setTo')->with(['receiver@example.com']); + $this->mailer->expects($this->once())->method('createMessage')->willReturn($message); + $this->mailer->expects($this->once())->method('createEMailTemplate')->with('sharebymail.RecipientPasswordNotification', [ + 'filename' => 'filename', + 'password' => 'password', + 'initiator' => 'owner', + 'initiatorEmail' => null, + 'shareWith' => 'receiver@example.com', + ]); + $this->mailer->expects($this->once())->method('send'); + + $this->assertSame('shareObject', + $instance->create($share) + ); + } + public function testCreateSendPasswordByMailWithEnforcedPasswordProtection() { $share = $this->getMockBuilder(IShare::class)->getMock(); $share->expects($this->any())->method('getSharedWith')->willReturn('receiver@example.com'); @@ -260,6 +305,10 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtection() { $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(null); + $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. $this->settingsManager->expects($this->any())->method('enforcePasswordProtection')->willReturn(true); $this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true); @@ -282,6 +331,51 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtection() { ); } + public function testCreateSendPasswordByMailWithPasswordAndWithEnforcedPasswordProtection() { + $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. + $this->settingsManager->expects($this->any())->method('enforcePasswordProtection')->willReturn(true); + $this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true); + $instance->expects($this->never())->method('autoGeneratePassword'); + + $message = $this->createMock(IMessage::class); + $message->expects($this->once())->method('setTo')->with(['receiver@example.com']); + $this->mailer->expects($this->once())->method('createMessage')->willReturn($message); + $this->mailer->expects($this->once())->method('createEMailTemplate')->with('sharebymail.RecipientPasswordNotification', [ + 'filename' => 'filename', + 'password' => 'password', + 'initiator' => 'owner', + 'initiatorEmail' => null, + 'shareWith' => 'receiver@example.com', + ]); + $this->mailer->expects($this->once())->method('send'); + + $this->assertSame('shareObject', + $instance->create($share) + ); + } + public function testCreateSendPasswordByTalkWithEnforcedPasswordProtection() { $share = $this->getMockBuilder(IShare::class)->getMock(); $share->expects($this->any())->method('getSharedWith')->willReturn('receiver@example.com'); @@ -300,6 +394,10 @@ public function testCreateSendPasswordByTalkWithEnforcedPasswordProtection() { $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(null); + $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 owner of the share. $this->settingsManager->expects($this->any())->method('enforcePasswordProtection')->willReturn(true); $this->settingsManager->expects($this->any())->method('sendPasswordByMail')->willReturn(true); From 26e4c292c7284ce6e1e3ddb52ae02123f19482b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:29:28 +0200 Subject: [PATCH 3/9] Fix enabling send password by Talk with empty password in link shares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When "send password by Talk" is enabled in a link share now a non empty password is enforced. Signed-off-by: Daniel Calviño Sánchez --- lib/private/Share20/Manager.php | 6 +++ tests/lib/Share20/ManagerTest.php | 69 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 73860fd39f42b..f88d884c0db56 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -972,8 +972,14 @@ public function updateShare(\OCP\Share\IShare $share) { } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { $this->linkCreateChecks($share); + $plainTextPassword = $share->getPassword(); + $this->updateSharePasswordIfNeeded($share, $originalShare); + if (empty($plainTextPassword) && $share->getSendPasswordByTalk()) { + throw new \InvalidArgumentException('Can’t enable sending the password by Talk with an empty password'); + } + if ($share->getExpirationDate() != $originalShare->getExpirationDate()) { //Verify the expiration date $this->validateExpirationDate($share); diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 9bc1531ac7fb9..962974b231d5e 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -2732,6 +2732,75 @@ public function testUpdateShareLink() { $manager->updateShare($share); } + public function testUpdateShareLinkEnableSendPasswordByTalkWithNoPassword() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Can’t enable sending the password by Talk with an empty password'); + + $manager = $this->createManagerMock() + ->setMethods([ + 'canShare', + 'getShareById', + 'generalCreateChecks', + 'linkCreateChecks', + 'pathCreateChecks', + 'verifyPassword', + 'validateExpirationDate', + ]) + ->getMock(); + + $originalShare = $this->manager->newShare(); + $originalShare->setShareType(\OCP\Share::SHARE_TYPE_LINK) + ->setPermissions(15); + + $tomorrow = new \DateTime(); + $tomorrow->setTime(0,0,0); + $tomorrow->add(new \DateInterval('P1D')); + + $file = $this->createMock(File::class); + $file->method('getId')->willReturn(100); + + $share = $this->manager->newShare(); + $share->setProviderId('foo') + ->setId('42') + ->setShareType(\OCP\Share::SHARE_TYPE_LINK) + ->setToken('token') + ->setSharedBy('owner') + ->setShareOwner('owner') + ->setPassword(null) + ->setSendPasswordByTalk(true) + ->setExpirationDate($tomorrow) + ->setNode($file) + ->setPermissions(15); + + $manager->expects($this->once())->method('canShare')->willReturn(true); + $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); + $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('linkCreateChecks')->with($share); + $manager->expects($this->never())->method('verifyPassword'); + $manager->expects($this->never())->method('pathCreateChecks'); + $manager->expects($this->never())->method('validateExpirationDate'); + + $this->hasher->expects($this->never()) + ->method('hash'); + + $this->defaultProvider->expects($this->never()) + ->method('update'); + + $hookListner = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListner, 'post'); + $hookListner->expects($this->never())->method('post'); + + $hookListner2 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_password', $hookListner2, 'post'); + $hookListner2->expects($this->never())->method('post'); + + $hookListner3 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_permissions', $hookListner3, 'post'); + $hookListner3->expects($this->never())->method('post'); + + $manager->updateShare($share); + } + public function testUpdateShareMail() { $manager = $this->createManagerMock() ->setMethods([ From 1c580351da069bbdfdb55559ca6800f11c6d6090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:37:18 +0200 Subject: [PATCH 4/9] Fix enabling send password by Talk with same password in mail shares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When "send password by Talk" is enabled in a mail share a new password must be also set. However, when the passwords of the original and the new share were compared it was not taken into account that the original password is now hashed, while the new one is not (unless no new password was sent, in which case the password of the original share was set in the new share by the controller, but that was already prevented due to both passwords being literally the same), so it was possible to set the same password again. Signed-off-by: Daniel Calviño Sánchez --- lib/private/Share20/Manager.php | 12 +++- tests/lib/Share20/ManagerTest.php | 100 ++++++++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index f88d884c0db56..08dca45a2996f 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -1085,8 +1085,14 @@ public function acceptShare(IShare $share, string $recipientId): IShare { * @return boolean whether the password was updated or not. */ private function updateSharePasswordIfNeeded(\OCP\Share\IShare $share, \OCP\Share\IShare $originalShare) { + $passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) && + (($share->getPassword() !== null && $originalShare->getPassword() === null) || + ($share->getPassword() === null && $originalShare->getPassword() !== null) || + ($share->getPassword() !== null && $originalShare->getPassword() !== null && + !$this->hasher->verify($share->getPassword(), $originalShare->getPassword()))); + // Password updated. - if ($share->getPassword() !== $originalShare->getPassword()) { + if ($passwordsAreDifferent) { //Verify the password $this->verifyPassword($share->getPassword()); @@ -1096,6 +1102,10 @@ private function updateSharePasswordIfNeeded(\OCP\Share\IShare $share, \OCP\Shar return true; } + } else { + // Reset the password to the original one, as it is either the same + // as the "new" password or a hashed version of it. + $share->setPassword($originalShare->getPassword()); } return false; diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 962974b231d5e..e0a0d9bfad2b2 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -2954,6 +2954,88 @@ public function testUpdateShareMailEnableSendPasswordByTalk() { $manager->updateShare($share); } + public function testUpdateShareMailEnableSendPasswordByTalkWithDifferentPassword() { + $manager = $this->createManagerMock() + ->setMethods([ + 'canShare', + 'getShareById', + 'generalCreateChecks', + 'verifyPassword', + 'pathCreateChecks', + 'linkCreateChecks', + 'validateExpirationDate', + ]) + ->getMock(); + + $originalShare = $this->manager->newShare(); + $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) + ->setPermissions(\OCP\Constants::PERMISSION_ALL) + ->setPassword('anotherPasswordHash') + ->setSendPasswordByTalk(false); + + $tomorrow = new \DateTime(); + $tomorrow->setTime(0,0,0); + $tomorrow->add(new \DateInterval('P1D')); + + $file = $this->createMock(File::class); + $file->method('getId')->willReturn(100); + + $share = $this->manager->newShare(); + $share->setProviderId('foo') + ->setId('42') + ->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) + ->setToken('token') + ->setSharedBy('owner') + ->setShareOwner('owner') + ->setPassword('password') + ->setSendPasswordByTalk(true) + ->setExpirationDate($tomorrow) + ->setNode($file) + ->setPermissions(\OCP\Constants::PERMISSION_ALL); + + $manager->expects($this->once())->method('canShare')->willReturn(true); + $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); + $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('verifyPassword')->with('password'); + $manager->expects($this->once())->method('pathCreateChecks')->with($file); + $manager->expects($this->never())->method('linkCreateChecks'); + $manager->expects($this->never())->method('validateExpirationDate'); + + $this->hasher->expects($this->once()) + ->method('verify') + ->with('password', 'anotherPasswordHash') + ->willReturn(false); + + $this->hasher->expects($this->once()) + ->method('hash') + ->with('password') + ->willReturn('hashed'); + + $this->defaultProvider->expects($this->once()) + ->method('update') + ->with($share, 'password') + ->willReturn($share); + + $hookListner = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListner, 'post'); + $hookListner->expects($this->never())->method('post'); + + $hookListner2 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_password', $hookListner2, 'post'); + $hookListner2->expects($this->once())->method('post')->with([ + 'itemType' => 'file', + 'itemSource' => 100, + 'uidOwner' => 'owner', + 'token' => 'token', + 'disabled' => false, + ]); + + $hookListner3 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_permissions', $hookListner3, 'post'); + $hookListner3->expects($this->never())->method('post'); + + $manager->updateShare($share); + } public function testUpdateShareMailEnableSendPasswordByTalkWithNoPassword() { $this->expectException(\InvalidArgumentException::class); @@ -3046,7 +3128,7 @@ public function testUpdateShareMailEnableSendPasswordByTalkRemovingPassword() { $originalShare = $this->manager->newShare(); $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) ->setPermissions(\OCP\Constants::PERMISSION_ALL) - ->setPassword('password') + ->setPassword('passwordHash') ->setSendPasswordByTalk(false); $tomorrow = new \DateTime(); @@ -3118,7 +3200,7 @@ public function testUpdateShareMailEnableSendPasswordByTalkRemovingPasswordWithE $originalShare = $this->manager->newShare(); $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) ->setPermissions(\OCP\Constants::PERMISSION_ALL) - ->setPassword('password') + ->setPassword('passwordHash') ->setSendPasswordByTalk(false); $tomorrow = new \DateTime(); @@ -3190,7 +3272,7 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithPreviousPassword( $originalShare = $this->manager->newShare(); $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) ->setPermissions(\OCP\Constants::PERMISSION_ALL) - ->setPassword('password') + ->setPassword('passwordHash') ->setSendPasswordByTalk(false); $tomorrow = new \DateTime(); @@ -3221,6 +3303,11 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithPreviousPassword( $manager->expects($this->never())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDate'); + $this->hasher->expects($this->once()) + ->method('verify') + ->with('password', 'passwordHash') + ->willReturn(true); + $this->hasher->expects($this->never()) ->method('hash'); @@ -3258,7 +3345,7 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword $originalShare = $this->manager->newShare(); $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) ->setPermissions(\OCP\Constants::PERMISSION_ALL) - ->setPassword('password') + ->setPassword('passwordHash') ->setSendPasswordByTalk(true); $tomorrow = new \DateTime(); @@ -3289,6 +3376,11 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword $manager->expects($this->never())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDate'); + $this->hasher->expects($this->once()) + ->method('verify') + ->with('password', 'passwordHash') + ->willReturn(true); + $this->hasher->expects($this->never()) ->method('hash'); From d6f193750212c2181bce32afe2220c0434cdacf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:40:33 +0200 Subject: [PATCH 5/9] Fix disabling send password by Talk without new password in mail shares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When "send password by Talk" was disabled in a mail share it was possible to keep the same password as before, as it does not pose any security issue (unlike keeping it when "send password by Talk" is enabled, as in that case the password was already disclosed by mail). However, if a mail share is updated but the password is not set again only the hashed password will be available. In that case it would not make sense to send the password by mail, so now the password must be changed when disabling "send password by Talk". Note that, even if explicitly setting the same password again along with the "send password by Talk" property would work, this was also prevented for simplicity. Signed-off-by: Daniel Calviño Sánchez --- lib/private/Share20/Manager.php | 8 +-- tests/lib/Share20/ManagerTest.php | 84 +++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 08dca45a2996f..fead605eb0379 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -987,11 +987,9 @@ public function updateShare(\OCP\Share\IShare $share) { } } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { // The new password is not set again if it is the same as the old - // one, unless when switching from sending by Talk to sending by - // mail. + // one. $plainTextPassword = $share->getPassword(); - if (!empty($plainTextPassword) && !$this->updateSharePasswordIfNeeded($share, $originalShare) && - !($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk())) { + if (!empty($plainTextPassword) && !$this->updateSharePasswordIfNeeded($share, $originalShare)) { $plainTextPassword = null; } if (empty($plainTextPassword) && !$originalShare->getSendPasswordByTalk() && $share->getSendPasswordByTalk()) { @@ -999,6 +997,8 @@ public function updateShare(\OCP\Share\IShare $share) { // would already have access to the share without having to call // the sharer to verify her identity throw new \InvalidArgumentException('Can’t enable sending the password by Talk without setting a new password'); + } elseif (empty($plainTextPassword) && $originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()) { + throw new \InvalidArgumentException('Can’t disable sending the password by Talk without setting a new password'); } } diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index e0a0d9bfad2b2..946c7192f376c 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -3330,6 +3330,9 @@ public function testUpdateShareMailEnableSendPasswordByTalkWithPreviousPassword( } public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Can’t disable sending the password by Talk without setting a new password'); + $manager = $this->createManagerMock() ->setMethods([ 'canShare', @@ -3371,8 +3374,8 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword $manager->expects($this->once())->method('canShare')->willReturn(true); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $manager->expects($this->once())->method('generalCreateChecks')->with($share); - $manager->expects($this->once())->method('pathCreateChecks')->with($file); $manager->expects($this->never())->method('verifyPassword'); + $manager->expects($this->never())->method('pathCreateChecks'); $manager->expects($this->never())->method('linkCreateChecks'); $manager->expects($this->never())->method('validateExpirationDate'); @@ -3384,10 +3387,8 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword $this->hasher->expects($this->never()) ->method('hash'); - $this->defaultProvider->expects($this->once()) - ->method('update') - ->with($share, 'password') - ->willReturn($share); + $this->defaultProvider->expects($this->never()) + ->method('update'); $hookListner = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); \OCP\Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListner, 'post'); @@ -3404,6 +3405,79 @@ public function testUpdateShareMailDisableSendPasswordByTalkWithPreviousPassword $manager->updateShare($share); } + public function testUpdateShareMailDisableSendPasswordByTalkWithoutChangingPassword() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Can’t disable sending the password by Talk without setting a new password'); + + $manager = $this->createManagerMock() + ->setMethods([ + 'canShare', + 'getShareById', + 'generalCreateChecks', + 'verifyPassword', + 'pathCreateChecks', + 'linkCreateChecks', + 'validateExpirationDate', + ]) + ->getMock(); + + $originalShare = $this->manager->newShare(); + $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) + ->setPermissions(\OCP\Constants::PERMISSION_ALL) + ->setPassword('passwordHash') + ->setSendPasswordByTalk(true); + + $tomorrow = new \DateTime(); + $tomorrow->setTime(0,0,0); + $tomorrow->add(new \DateInterval('P1D')); + + $file = $this->createMock(File::class); + $file->method('getId')->willReturn(100); + + $share = $this->manager->newShare(); + $share->setProviderId('foo') + ->setId('42') + ->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) + ->setToken('token') + ->setSharedBy('owner') + ->setShareOwner('owner') + ->setPassword('passwordHash') + ->setSendPasswordByTalk(false) + ->setExpirationDate($tomorrow) + ->setNode($file) + ->setPermissions(\OCP\Constants::PERMISSION_ALL); + + $manager->expects($this->once())->method('canShare')->willReturn(true); + $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); + $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->never())->method('verifyPassword'); + $manager->expects($this->never())->method('pathCreateChecks'); + $manager->expects($this->never())->method('linkCreateChecks'); + $manager->expects($this->never())->method('validateExpirationDate'); + + $this->hasher->expects($this->never()) + ->method('verify'); + + $this->hasher->expects($this->never()) + ->method('hash'); + + $this->defaultProvider->expects($this->never()) + ->method('update'); + + $hookListner = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListner, 'post'); + $hookListner->expects($this->never())->method('post'); + + $hookListner2 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_password', $hookListner2, 'post'); + $hookListner2->expects($this->never())->method('post'); + + $hookListner3 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_permissions', $hookListner3, 'post'); + $hookListner3->expects($this->never())->method('post'); + + $manager->updateShare($share); + } public function testMoveShareLink() { $this->expectException(\InvalidArgumentException::class); From 8349172c64006aedbc87058aed6b159196a00c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:46:33 +0200 Subject: [PATCH 6/9] Remove unused variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The step names were adjusted accordingly. Signed-off-by: Daniel Calviño Sánchez --- build/integration/features/bootstrap/Sharing.php | 11 ++++------- build/integration/sharing_features/sharing-v1.feature | 6 +++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index 66b15436438f4..f13abd6efadbd 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -150,11 +150,9 @@ public function userAcceptsLastShare(string $user) { } /** - * @Then /^Public shared file "([^"]*)" can be downloaded$/ + * @Then /^last link share can be downloaded$/ */ - public function checkPublicSharedFile($filename) { - $client = new Client(); - $options = []; + public function lastLinkShareCanBeDownloaded() { if (count($this->lastShareData->data->element) > 0){ $url = $this->lastShareData->data[0]->url; } @@ -166,10 +164,9 @@ public function checkPublicSharedFile($filename) { } /** - * @Then /^Public shared file "([^"]*)" with password "([^"]*)" can be downloaded$/ + * @Then /^last link share with password "([^"]*)" can be downloaded$/ */ - public function checkPublicSharedFileWithPassword($filename, $password) { - $options = []; + public function lastLinkShareWithPasswordCanBeDownloaded($password) { if (count($this->lastShareData->data->element) > 0){ $token = $this->lastShareData->data[0]->token; } diff --git a/build/integration/sharing_features/sharing-v1.feature b/build/integration/sharing_features/sharing-v1.feature index 51a627dc6dec9..8440a2d874e92 100644 --- a/build/integration/sharing_features/sharing-v1.feature +++ b/build/integration/sharing_features/sharing-v1.feature @@ -64,7 +64,7 @@ Feature: sharing | shareType | 3 | Then the OCS status code should be "100" And the HTTP status code should be "200" - And Public shared file "welcome.txt" can be downloaded + And last link share can be downloaded Scenario: Creating a new public share with password Given user "user0" exists @@ -75,7 +75,7 @@ Feature: sharing | password | publicpw | Then the OCS status code should be "100" And the HTTP status code should be "200" - And Public shared file "welcome.txt" with password "publicpw" can be downloaded + And last link share with password "publicpw" can be downloaded Scenario: Creating a new public share of a folder Given user "user0" exists @@ -108,7 +108,7 @@ Feature: sharing | expireDate | +3 days | Then the OCS status code should be "100" And the HTTP status code should be "200" - And Public shared file "welcome.txt" with password "publicpw" can be downloaded + And last link share with password "publicpw" can be downloaded Scenario: Creating a new public share, updating its expiration date and getting its info Given user "user0" exists From c8dafceb36ce80707f346ae4a08699d54849591c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:50:02 +0200 Subject: [PATCH 7/9] Generalize integration test steps to download last share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that the "last link share can be downloaded" step was kept as it tests the "url" property specific of link shares. Signed-off-by: Daniel Calviño Sánchez --- .../integration/features/bootstrap/Sharing.php | 18 ++++++++++++++++-- .../sharing_features/sharing-v1.feature | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index f13abd6efadbd..fee8f7d269232 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -164,9 +164,23 @@ public function lastLinkShareCanBeDownloaded() { } /** - * @Then /^last link share with password "([^"]*)" can be downloaded$/ + * @Then /^last share can be downloaded$/ */ - public function lastLinkShareWithPasswordCanBeDownloaded($password) { + public function lastShareCanBeDownloaded() { + if (count($this->lastShareData->data->element) > 0) { + $token = $this->lastShareData->data[0]->token; + } else { + $token = $this->lastShareData->data->token; + } + + $fullUrl = substr($this->baseUrl, 0, -4) . "index.php/s/" . $token . "/download"; + $this->checkDownload($fullUrl, null, 'text/plain'); + } + + /** + * @Then /^last share with password "([^"]*)" can be downloaded$/ + */ + public function lastShareWithPasswordCanBeDownloaded($password) { if (count($this->lastShareData->data->element) > 0){ $token = $this->lastShareData->data[0]->token; } diff --git a/build/integration/sharing_features/sharing-v1.feature b/build/integration/sharing_features/sharing-v1.feature index 8440a2d874e92..498fe1456f68d 100644 --- a/build/integration/sharing_features/sharing-v1.feature +++ b/build/integration/sharing_features/sharing-v1.feature @@ -75,7 +75,7 @@ Feature: sharing | password | publicpw | Then the OCS status code should be "100" And the HTTP status code should be "200" - And last link share with password "publicpw" can be downloaded + And last share with password "publicpw" can be downloaded Scenario: Creating a new public share of a folder Given user "user0" exists @@ -108,7 +108,7 @@ Feature: sharing | expireDate | +3 days | Then the OCS status code should be "100" And the HTTP status code should be "200" - And last link share with password "publicpw" can be downloaded + And last share with password "publicpw" can be downloaded Scenario: Creating a new public share, updating its expiration date and getting its info Given user "user0" exists From f5eb93e29adf97bbbcbff976bed29ef353abbff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:52:11 +0200 Subject: [PATCH 8/9] Add integration tests for creating and updating a mail share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In most cases, when a mail share is created or updated an e-mail is sent to the sharee, which is done by connecting to the SMTP server set in the configuration. If the server can not be contacted then the creation or update of the mail share fails. To make possible to test mail shares without using a real SMTP server a fake one has been added. The original script, which is MIT licensed, was based on inetd, so it was slightly modified to run on its own. In order to use it from the integration tests the "Given dummy mail server is listening" step has to be called in the scenarios in which the mail server is needed. For now that is the only available step; things like checking the sent mails, while possible (as the script can log the mails to certain file), have not been added yet. Signed-off-by: Daniel Calviño Sánchez --- .../features/bootstrap/BasicStructure.php | 1 + .../features/bootstrap/FakeSMTPHelper.php | 159 ++++++++++++++++++ build/integration/features/bootstrap/Mail.php | 57 +++++++ .../features/bootstrap/SharingContext.php | 5 +- .../sharing_features/sharing-v1.feature | 86 ++++++++++ 5 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 build/integration/features/bootstrap/FakeSMTPHelper.php create mode 100644 build/integration/features/bootstrap/Mail.php diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index 03cdd58833e90..20d5727a86c90 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -46,6 +46,7 @@ trait BasicStructure { use Auth; use Download; + use Mail; use Trashbin; /** @var string */ diff --git a/build/integration/features/bootstrap/FakeSMTPHelper.php b/build/integration/features/bootstrap/FakeSMTPHelper.php new file mode 100644 index 0000000000000..9a64b6d31dec2 --- /dev/null +++ b/build/integration/features/bootstrap/FakeSMTPHelper.php @@ -0,0 +1,159 @@ +server interaction + * The comunication is based upon the SMPT standards defined in http://www.lesnikowski.com/mail/Rfc/rfc2821.txt + */ + +class fakeSMTP { + public $logFile = false; + public $serverHello = 'fakeSMTP ESMTP PHP Mail Server Ready'; + + public function __construct($fd) { + $this->mail = []; + $this->mail['ipaddress'] = false; + $this->mail['emailSender'] = ''; + $this->mail['emailRecipients'] = []; + $this->mail['emailSubject'] = false; + $this->mail['rawEmail'] = false; + $this->mail['emailHeaders'] = false; + $this->mail['emailBody'] = false; + + $this->fd = $fd; + } + + public function receive() { + $hasValidFrom = false; + $hasValidTo = false; + $receivingData = false; + $header = true; + $this->reply('220 '.$this->serverHello); + $this->mail['ipaddress'] = $this->detectIP(); + while ($data = fgets($this->fd)) { + $data = preg_replace('@\r\n@', "\n", $data); + + if (!$receivingData) { + $this->log($data); + } + + if (!$receivingData && preg_match('/^MAIL FROM:\s?<(.*)>/i', $data, $match)) { + if (preg_match('/(.*)@\[.*\]/i', $match[1]) || $match[1] != '' || $this->validateEmail($match[1])) { + $this->mail['emailSender'] = $match[1]; + $this->reply('250 2.1.0 Ok'); + $hasValidFrom = true; + } else { + $this->reply('551 5.1.7 Bad sender address syntax'); + } + } elseif (!$receivingData && preg_match('/^RCPT TO:\s?<(.*)>/i', $data, $match)) { + if (!$hasValidFrom) { + $this->reply('503 5.5.1 Error: need MAIL command'); + } else { + if (preg_match('/postmaster@\[.*\]/i', $match[1]) || $this->validateEmail($match[1])) { + array_push($this->mail['emailRecipients'], $match[1]); + $this->reply('250 2.1.5 Ok'); + $hasValidTo = true; + } else { + $this->reply('501 5.1.3 Bad recipient address syntax '.$match[1]); + } + } + } elseif (!$receivingData && preg_match('/^RSET$/i', trim($data))) { + $this->reply('250 2.0.0 Ok'); + $hasValidFrom = false; + $hasValidTo = false; + } elseif (!$receivingData && preg_match('/^NOOP$/i', trim($data))) { + $this->reply('250 2.0.0 Ok'); + } elseif (!$receivingData && preg_match('/^VRFY (.*)/i', trim($data), $match)) { + $this->reply('250 2.0.0 '.$match[1]); + } elseif (!$receivingData && preg_match('/^DATA/i', trim($data))) { + if (!$hasValidTo) { + $this->reply('503 5.5.1 Error: need RCPT command'); + } else { + $this->reply('354 Ok Send data ending with .'); + $receivingData = true; + } + } elseif (!$receivingData && preg_match('/^(HELO|EHLO)/i', $data)) { + $this->reply('250 HELO '.$this->mail['ipaddress']); + } elseif (!$receivingData && preg_match('/^QUIT/i', trim($data))) { + break; + } elseif (!$receivingData) { + //~ $this->reply('250 Ok'); + $this->reply('502 5.5.2 Error: command not recognized'); + } elseif ($receivingData && $data == ".\n") { + /* Email Received, now let's look at it */ + $receivingData = false; + $this->reply('250 2.0.0 Ok: queued as '.$this->generateRandom(10)); + $splitmail = explode("\n\n", $this->mail['rawEmail'], 2); + if (count($splitmail) == 2) { + $this->mail['emailHeaders'] = $splitmail[0]; + $this->mail['emailBody'] = $splitmail[1]; + $headers = preg_replace("/ \s+/", ' ', preg_replace("/\n\s/", ' ', $this->mail['emailHeaders'])); + $headerlines = explode("\n", $headers); + for ($i=0; $imail['emailSubject'] = trim($matches[1]); + } + } + } else { + $this->mail['emailBody'] = $splitmail[0]; + } + set_time_limit(5); // Just run the exit to prevent open threads / abuse + } elseif ($receivingData) { + $this->mail['rawEmail'] .= $data; + } + } + /* Say good bye */ + $this->reply('221 2.0.0 Bye '.$this->mail['ipaddress']); + + fclose($this->fd); + } + + public function log($s) { + if ($this->logFile) { + file_put_contents($this->logFile, trim($s)."\n", FILE_APPEND); + } + } + + private function reply($s) { + $this->log("REPLY:$s"); + fwrite($this->fd, $s . "\r\n"); + } + + private function detectIP() { + $raw = explode(':', stream_socket_get_name($this->fd, true)); + return $raw[0]; + } + + private function validateEmail($email) { + return preg_match('/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/', strtolower($email)); + } + + private function generateRandom($length=8) { + $password = ''; + $possible = '2346789BCDFGHJKLMNPQRTVWXYZ'; + $maxlength = strlen($possible); + $i = 0; + for ($i=0; $i < $length; $i++) { + $char = substr($possible, mt_rand(0, $maxlength-1), 1); + if (!strstr($password, $char)) { + $password .= $char; + } + } + return $password; + } +} + +$socket = stream_socket_server('tcp://127.0.0.1:2525', $errno, $errstr); +if (!$socket) { + exit(); +} + +while ($fd = stream_socket_accept($socket)) { + $fakeSMTP = new fakeSMTP($fd); + $fakeSMTP->receive(); +} + +fclose($socket); diff --git a/build/integration/features/bootstrap/Mail.php b/build/integration/features/bootstrap/Mail.php new file mode 100644 index 0000000000000..d347636c8aa14 --- /dev/null +++ b/build/integration/features/bootstrap/Mail.php @@ -0,0 +1,57 @@ + + * + * @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 . + * + */ + +trait Mail { + + // CommandLine trait is expected to be used in the class that uses this + // trait. + + /** + * @var string + */ + private $fakeSmtpServerPid; + + /** + * @AfterScenario + */ + public function killDummyMailServer() { + if (!$this->fakeSmtpServerPid) { + return; + } + + exec("kill " . $this->fakeSmtpServerPid); + + $this->invokingTheCommand('config:system:delete mail_smtpport'); + } + + /** + * @Given /^dummy mail server is listening$/ + */ + public function dummyMailServerIsListening() { + // Default smtpport (25) is restricted for regular users, so the + // FakeSMTP uses 2525 instead. + $this->invokingTheCommand('config:system:set mail_smtpport --value=2525 --type integer'); + + $this->fakeSmtpServerPid = exec("php features/bootstrap/FakeSMTPHelper.php >/dev/null 2>&1 & echo $!"); + } +} diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php index cace64e4f8af5..1c2ce694e0f0b 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -34,6 +34,9 @@ class SharingContext implements Context, SnippetAcceptingContext { use Sharing; use AppConfiguration; + use CommandLine; - protected function resetAppConfigs() {} + protected function resetAppConfigs() { + $this->modifyServerConfig('sharebymail', 'enforcePasswordProtection', 'no'); + } } diff --git a/build/integration/sharing_features/sharing-v1.feature b/build/integration/sharing_features/sharing-v1.feature index 498fe1456f68d..764cd857a3ec5 100644 --- a/build/integration/sharing_features/sharing-v1.feature +++ b/build/integration/sharing_features/sharing-v1.feature @@ -56,6 +56,92 @@ Feature: sharing Then the OCS status code should be "403" And the HTTP status code should be "401" + Scenario: Creating a new mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share can be downloaded + + Scenario: Creating a new mail share with password + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + | password | publicpw | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "publicpw" can be downloaded + + Scenario: Creating a new mail share with password when password protection is enforced + Given dummy mail server is listening + And As an "admin" + And parameter "enforcePasswordProtection" of app "sharebymail" is set to "yes" + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + | password | publicpw | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "publicpw" can be downloaded + + Scenario: Creating a new mail share and setting a password + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + And Updating last share with + | password | publicpw | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "publicpw" can be downloaded + + Scenario: Creating a new mail share and setting a password twice + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + And Updating last share with + | password | publicpw | + And Updating last share with + | password | another publicpw | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another publicpw" can be downloaded + + Scenario: Creating a new mail share and setting the same password twice + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dumy@test.com | + And Updating last share with + | password | publicpw | + And Updating last share with + | password | publicpw | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "publicpw" can be downloaded + Scenario: Creating a new public share Given user "user0" exists And As an "user0" From b5ffa2ea3d3d38dcf68b1ba3cd65998621256512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 28 May 2020 20:53:36 +0200 Subject: [PATCH 9/9] Add integration tests for video verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling the "send password by Talk" property of shares require that Talk is installed and enabled, so the Drone step that runs them has to first clone the Talk repository. When the integration tests are run on a local development instance, however, it is not guaranteed that Talk is installed. Due to this the "@Talk" tag was added, which ensures that any feature or scenario marked with it will first check if Talk is installed and, if not, skip the scenario (instead of failing). Signed-off-by: Daniel Calviño Sánchez --- .drone.yml | 31 ++ build/integration/config/behat.yml | 1 + .../features/bootstrap/TalkContext.php | 72 +++ .../sharing-v1-video-verification.feature | 504 ++++++++++++++++++ 4 files changed, 608 insertions(+) create mode 100644 build/integration/features/bootstrap/TalkContext.php create mode 100644 build/integration/sharing_features/sharing-v1-video-verification.feature diff --git a/.drone.yml b/.drone.yml index 2e2e9e8f95902..e0671a050a592 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1417,6 +1417,37 @@ trigger: - pull_request - push +--- +kind: pipeline +name: integration-sharing-v1-video-verification + +steps: +- name: submodules + image: docker:git + commands: + - git submodule update --init +- name: install-talk + image: docker:git + commands: + # JavaScript files are not used in integration tests so it is not needed to + # build them. + - git clone --branch stable18 --depth 1 https://github.com/nextcloud/spreed apps/spreed +- name: integration-sharing-v1-video-verification + image: nextcloudci/integration-php7.3:integration-php7.3-2 + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - ./occ maintenance:install --admin-pass=admin --data-dir=/dev/shm/nc_int + - cd build/integration + - ./run.sh sharing_features/sharing-v1-video-verification.feature + +trigger: + branch: + - master + - stable* + event: + - pull_request + - push + --- kind: pipeline name: integration-setup-features diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml index 27d7daa481812..79ffe58f6b682 100644 --- a/build/integration/config/behat.yml +++ b/build/integration/config/behat.yml @@ -65,6 +65,7 @@ default: - admin - admin regular_user_password: 123456 + - TalkContext setup: paths: - "%paths.base%/../setup_features" diff --git a/build/integration/features/bootstrap/TalkContext.php b/build/integration/features/bootstrap/TalkContext.php new file mode 100644 index 0000000000000..bc61c87ebab18 --- /dev/null +++ b/build/integration/features/bootstrap/TalkContext.php @@ -0,0 +1,72 @@ + + * + * @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 . + * + */ + +use Behat\Behat\Context\Context; + +class TalkContext implements Context { + + /** + * @BeforeFeature @Talk + * @BeforeScenario @Talk + */ + public static function skipTestsIfTalkIsNotInstalled() { + if (!TalkContext::isTalkInstalled()) { + throw new Exception('Talk needs to be installed to run features or scenarios tagged with @Talk'); + } + } + + /** + * @AfterScenario @Talk + */ + public static function disableTalk() { + TalkContext::runOcc(['app:disable', 'spreed']); + } + + private static function isTalkInstalled(): bool { + $appList = TalkContext::runOcc(['app:list']); + + return strpos($appList, 'spreed') !== false; + } + + private static function runOcc(array $args): string { + // Based on "runOcc" from CommandLine trait (which can not be used due + // to not being static and being already used in other sibling + // contexts). + $args = array_map(function ($arg) { + return escapeshellarg($arg); + }, $args); + $args[] = '--no-ansi --no-warnings'; + $args = implode(' ', $args); + + $descriptor = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = proc_open('php console.php ' . $args, $descriptor, $pipes, $ocPath = '../..'); + $lastStdOut = stream_get_contents($pipes[1]); + proc_close($process); + + return $lastStdOut; + } +} diff --git a/build/integration/sharing_features/sharing-v1-video-verification.feature b/build/integration/sharing_features/sharing-v1-video-verification.feature new file mode 100644 index 0000000000000..e57ec9a9f98a2 --- /dev/null +++ b/build/integration/sharing_features/sharing-v1-video-verification.feature @@ -0,0 +1,504 @@ +@Talk +Feature: sharing + Background: + Given using api version "1" + Given using old dav path + Given invoking occ with "app:enable spreed" + + Scenario: Creating a link share with send password by Talk + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk with different password in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + Scenario: Enabling send password by Talk with different password set after creation in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + Scenario: Enabling send password by Talk with same password in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk with same password set after creation in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk without updating password in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk without updating password set after creation in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk with no password in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share can be downloaded + + Scenario: Enabling send password by Talk with no password removed after creation in a link share + Given user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + And Updating last share with + | password | | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share can be downloaded + + Scenario: Disabling send password by Talk without setting new password in a link share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk without setting new password set after creation in a link share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk setting same password in a link share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk setting same password set after creation in a link share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk setting new password in a link share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + Scenario: Disabling send password by Talk setting new password set after creation in a link share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 3 | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + + + + + Scenario: Creating a mail share with send password by Talk + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk with different password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + Scenario: Enabling send password by Talk with different password set after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + Scenario: Enabling send password by Talk with same password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk with same password set after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk without updating password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk without updating password set after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Enabling send password by Talk with no password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share can be downloaded + + Scenario: Enabling send password by Talk with no password removed after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + And Updating last share with + | password | | + And Updating last share with + | sendPasswordByTalk | true | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share can be downloaded + + Scenario: Disabling send password by Talk without setting new password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | sendPasswordByTalk | false | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk without setting new password set after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | sendPasswordByTalk | false | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk setting same password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk setting same password set after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "400" + And the HTTP status code should be "200" + And last share with password "secret" can be downloaded + + Scenario: Disabling send password by Talk setting new password in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded + + Scenario: Disabling send password by Talk setting new password set after creation in a mail share + Given dummy mail server is listening + And user "user0" exists + And As an "user0" + When creating a share with + | path | welcome.txt | + | shareType | 4 | + | shareWith | dummy@test.com | + And Updating last share with + | password | secret | + | sendPasswordByTalk | true | + And Updating last share with + | password | another secret | + | sendPasswordByTalk | false | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And last share with password "another secret" can be downloaded