From 3121e4a39235e5069d2f62d6f221587ac7a21630 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Mon, 7 Mar 2022 17:43:55 -0100 Subject: [PATCH 1/2] allow one single password per Circles Signed-off-by: Maxence Lange --- appinfo/info.xml | 1 + appinfo/routes.php | 4 +- lib/Circles/FileSharingBroadcaster.php | 34 ++-- lib/Command/CirclesSetting.php | 171 ++++++++++++++++++ lib/Controller/AdminController.php | 12 +- lib/Controller/LocalController.php | 10 +- lib/Db/CircleRequest.php | 12 ++ lib/FederatedItems/CircleSetting.php | 118 ++++++++++++ lib/GlobalScale/FileShare.php | 22 +-- lib/GlobalScale/MemberAdd.php | 18 +- .../Files/PreparingMemberSendMail.php | 29 +-- .../Files/PreparingShareSendMail.php | 21 ++- lib/Model/Circle.php | 38 +++- lib/Service/CircleService.php | 53 ++++-- lib/Service/ConfigService.php | 73 ++++---- lib/Service/SendMailService.php | 46 ++++- 16 files changed, 528 insertions(+), 134 deletions(-) create mode 100644 lib/Command/CirclesSetting.php create mode 100644 lib/FederatedItems/CircleSetting.php diff --git a/appinfo/info.xml b/appinfo/info.xml index e8d34aae0..0768c3111 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -57,6 +57,7 @@ Those groups of users (or "circles") can then be used by any other app for shari OCA\Circles\Command\CirclesDestroy OCA\Circles\Command\CirclesDetails OCA\Circles\Command\CirclesConfig + OCA\Circles\Command\CirclesSetting OCA\Circles\Command\CirclesSync OCA\Circles\Command\CirclesCheck OCA\Circles\Command\CirclesTest diff --git a/appinfo/routes.php b/appinfo/routes.php index 13b0e5ebb..9b4b6d21e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -58,7 +58,7 @@ ['name' => 'Local#editName', 'url' => '/circles/{circleId}/name', 'verb' => 'PUT'], ['name' => 'Local#editDescription', 'url' => '/circles/{circleId}/description', 'verb' => 'PUT'], - ['name' => 'Local#editSettings', 'url' => '/circles/{circleId}/settings', 'verb' => 'PUT'], + ['name' => 'Local#editSetting', 'url' => '/circles/{circleId}/setting', 'verb' => 'PUT'], ['name' => 'Local#editConfig', 'url' => '/circles/{circleId}/config', 'verb' => 'PUT'], ['name' => 'Local#link', 'url' => '/link/{circleId}/{singleId}', 'verb' => 'GET'], @@ -95,7 +95,7 @@ ['name' => 'Admin#circleLeave', 'url' => '/admin/{emulated}/circles/{circleId}/leave', 'verb' => 'PUT'], ['name' => 'Admin#editName', 'url' => '/admin/{emulated}/circles/{circleId}/name', 'verb' => 'PUT'], ['name' => 'Admin#editDescription', 'url' => '/admin/{emulated}/circles/{circleId}/description', 'verb' => 'PUT'], - ['name' => 'Admin#editSettings', 'url' => '/admin/{emulated}/circles/{circleId}/settings', 'verb' => 'PUT'], + ['name' => 'Admin#editSetting', 'url' => '/admin/{emulated}/circles/{circleId}/setting', 'verb' => 'PUT'], ['name' => 'Admin#editConfig', 'url' => '/admin/{emulated}/circles/{circleId}/config', 'verb' => 'PUT'], ['name' => 'Admin#link', 'url' => '/admin/{emulated}/link/{circleId}/{singleId}', 'verb' => 'GET'] ], diff --git a/lib/Circles/FileSharingBroadcaster.php b/lib/Circles/FileSharingBroadcaster.php index 89cc376a4..316ab8358 100644 --- a/lib/Circles/FileSharingBroadcaster.php +++ b/lib/Circles/FileSharingBroadcaster.php @@ -220,14 +220,14 @@ public function createShareToMember(SharingFrame $frame, DeprecatedMember $membe $password = ''; $sendPasswordByMail = true; - if ($this->configService->enforcePasswordProtection($circle)) { - if ($circle->getSetting('password_single_enabled') === 'true') { - $password = $circle->getPasswordSingle(); - $sendPasswordByMail = false; - } else { - $password = $this->miscService->token(15); - } - } +// if ($this->configService->enforcePasswordProtection($circle)) { +// if ($circle->getSetting('password_single_enabled') === 'true') { +// $password = $circle->getPasswordSingle(); +// $sendPasswordByMail = false; +// } else { +// $password = $this->miscService->token(15); +// } +// } $sharesToken = $this->tokensRequest->generateTokenForMember($member, $share->getId(), $password); @@ -473,9 +473,9 @@ protected function sendMail($fileName, $link, $author, $circleName, $email) { * @throws Exception */ protected function sendPasswordByMail(IShare $share, $circleName, $email, $password) { - if (!$this->configService->sendPasswordByMail() || $password === '') { - return; - } +// if (!$this->configService->sendPasswordByMail() || $password === '') { +// return; +// } $message = $this->mailer->createMessage(); @@ -594,9 +594,9 @@ public function sendMailExitingShares( $data = []; $password = ''; - if ($this->configService->enforcePasswordProtection($circle)) { - $password = $this->miscService->token(15); - } +// if ($this->configService->enforcePasswordProtection($circle)) { +// $password = $this->miscService->token(15); +// } foreach ($unknownShares as $share) { try { @@ -628,9 +628,9 @@ public function sendMailExitingShares( * @throws Exception */ protected function sendPasswordExistingShares(DeprecatedMember $author, string $email, string $password) { - if (!$this->configService->sendPasswordByMail() || $password === '') { - return; - } +// if (!$this->configService->sendPasswordByMail() || $password === '') { +// return; +// } $message = $this->mailer->createMessage(); diff --git a/lib/Command/CirclesSetting.php b/lib/Command/CirclesSetting.php new file mode 100644 index 000000000..6044632cf --- /dev/null +++ b/lib/Command/CirclesSetting.php @@ -0,0 +1,171 @@ + + * @copyright 2022 + * @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 . + * + */ + + +namespace OCA\Circles\Command; + +use OC\Core\Command\Base; +use OCA\Circles\Exceptions\CircleNotFoundException; +use OCA\Circles\Exceptions\FederatedEventException; +use OCA\Circles\Exceptions\FederatedItemException; +use OCA\Circles\Exceptions\FederatedUserException; +use OCA\Circles\Exceptions\FederatedUserNotFoundException; +use OCA\Circles\Exceptions\InitiatorNotConfirmedException; +use OCA\Circles\Exceptions\InitiatorNotFoundException; +use OCA\Circles\Exceptions\InvalidIdException; +use OCA\Circles\Exceptions\MemberNotFoundException; +use OCA\Circles\Exceptions\OwnerNotFoundException; +use OCA\Circles\Exceptions\RemoteInstanceException; +use OCA\Circles\Exceptions\RemoteNotFoundException; +use OCA\Circles\Exceptions\RemoteResourceNotFoundException; +use OCA\Circles\Exceptions\RequestBuilderException; +use OCA\Circles\Exceptions\SingleCircleNotFoundException; +use OCA\Circles\Exceptions\UnknownRemoteException; +use OCA\Circles\Exceptions\UserTypeNotFoundException; +use OCA\Circles\Model\Helpers\MemberHelper; +use OCA\Circles\Model\Member; +use OCA\Circles\Service\CircleService; +use OCA\Circles\Service\FederatedUserService; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class CirclesSetting extends Base { + + + /** @var FederatedUserService */ + private $federatedUserService; + + /** @var CircleService */ + private $circleService; + + + /** + * @param FederatedUserService $federatedUserService + * @param CircleService $circlesService + */ + public function __construct(FederatedUserService $federatedUserService, CircleService $circlesService) { + parent::__construct(); + + $this->federatedUserService = $federatedUserService; + $this->circleService = $circlesService; + } + + + /** + * + */ + protected function configure() { + parent::configure(); + $this->setName('circles:manage:setting') + ->setDescription('edit setting for a Circle') + ->addArgument('circle_id', InputArgument::REQUIRED, 'ID of the circle') + ->addArgument('setting', InputArgument::OPTIONAL, 'setting to edit', '') + ->addArgument('value', InputArgument::OPTIONAL, 'value', '') + ->addOption('unset', '', InputOption::VALUE_NONE, 'unset the setting') + ->addOption('initiator', '', InputOption::VALUE_REQUIRED, 'set an initiator to the request', '') + ->addOption('initiator-type', '', InputOption::VALUE_REQUIRED, 'set initiator type', '0') + ->addOption('status-code', '', InputOption::VALUE_NONE, 'display status code on exception'); + } + + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + * @throws FederatedEventException + * @throws FederatedItemException + * @throws InitiatorNotFoundException + * @throws RequestBuilderException + * @throws CircleNotFoundException + * @throws FederatedUserException + * @throws FederatedUserNotFoundException + * @throws InitiatorNotConfirmedException + * @throws InvalidIdException + * @throws MemberNotFoundException + * @throws OwnerNotFoundException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws SingleCircleNotFoundException + * @throws UnknownRemoteException + * @throws UserTypeNotFoundException + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $circleId = (string)$input->getArgument('circle_id'); + $setting = (string)$input->getArgument('setting'); + $value = (string)$input->getArgument('value'); + + try { + $this->federatedUserService->commandLineInitiator( + $input->getOption('initiator'), + Member::parseTypeString($input->getOption('initiator-type')), + $circleId, + false + ); + + if ($setting === '') { + $circle = $this->circleService->getCircle($circleId); + $initiatorHelper = new MemberHelper($circle->getInitiator()); + $initiatorHelper->mustBeAdmin(); + $output->writeln(json_encode($circle->getSettings(), JSON_PRETTY_PRINT)); + + return 0; + } + + if (!$input->getOption('unset') && $value === '') { + throw new InvalidArgumentException('you need to specify a value'); + } + + $outcome = $this->circleService->updateSetting( + $circleId, + $setting, + ($input->getOption('unset')) ? null : $value, + ); + } catch (FederatedItemException $e) { + if ($input->getOption('status-code')) { + throw new FederatedItemException( + ' [' . get_class($e) . ', ' . $e->getStatus() . ']' . "\n" . $e->getMessage() + ); + } + + throw $e; + } + + if (strtolower($input->getOption('output')) === 'json') { + $output->writeln(json_encode($outcome, JSON_PRETTY_PRINT)); + } + + return 0; + } +} diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 0919c0e62..ed0aefaa6 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -462,25 +462,29 @@ public function editDescription(string $emulated, string $circleId, string $valu /** * @param string $emulated * @param string $circleId - * @param array $value + * @param string $setting + * @param string|null $value * * @return DataResponse * @throws OCSException */ - public function editSettings(string $emulated, string $circleId, array $value): DataResponse { + public function editSetting(string $emulated, string $circleId, string $setting, ?string $value = null): DataResponse { try { $this->setLocalFederatedUser($emulated); - $outcome = $this->circleService->updateSettings($circleId, $value); + $outcome = $this->circleService->updateSetting($circleId, $setting, $value); return new DataResponse($this->serializeArray($outcome)); } catch (Exception $e) { - $this->e($e, ['emulated' => $emulated, 'circleId' => $circleId, 'value' => $value]); + $this->e($e, ['circleId' => $circleId, 'setting' => $setting, 'value' => $value]); throw new OCSException($e->getMessage(), $e->getCode()); } } + + + /** * @param string $emulated * @param string $circleId diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php index 35ceee4fb..4a6ec8225 100644 --- a/lib/Controller/LocalController.php +++ b/lib/Controller/LocalController.php @@ -413,6 +413,7 @@ public function memberRemove(string $circleId, string $memberId): DataResponse { * * @param int $limit * @param int $offset + * * @return DataResponse * @throws OCSException */ @@ -505,20 +506,21 @@ public function editDescription(string $circleId, string $value): DataResponse { * @NoAdminRequired * * @param string $circleId - * @param array $value + * @param string $setting + * @param string|null $value * * @return DataResponse * @throws OCSException */ - public function editSettings(string $circleId, array $value): DataResponse { + public function editSetting(string $circleId, string $setting, ?string $value = null): DataResponse { try { $this->setCurrentFederatedUser(); - $outcome = $this->circleService->updateSettings($circleId, $value); + $outcome = $this->circleService->updateSetting($circleId, $setting, $value); return new DataResponse($this->serializeArray($outcome)); } catch (Exception $e) { - $this->e($e, ['circleId' => $circleId, 'value' => $value]); + $this->e($e, ['circleId' => $circleId, 'setting' => $setting, 'value' => $value]); throw new OCSException($e->getMessage(), $e->getCode()); } } diff --git a/lib/Db/CircleRequest.php b/lib/Db/CircleRequest.php index e3fef6ad3..b1e810d3b 100644 --- a/lib/Db/CircleRequest.php +++ b/lib/Db/CircleRequest.php @@ -150,6 +150,18 @@ public function updateConfig(Circle $circle) { } + /** + * @param Circle $circle + */ + public function updateSettings(Circle $circle) { + $qb = $this->getCircleUpdateSql(); + $qb->set('settings', $qb->createNamedParameter(json_encode($circle->getSettings()))); + $qb->limitToUniqueId($circle->getSingleId()); + + $qb->execute(); + } + + /** * @param IFederatedUser|null $initiator * @param CircleProbe $probe diff --git a/lib/FederatedItems/CircleSetting.php b/lib/FederatedItems/CircleSetting.php new file mode 100644 index 000000000..167a96a11 --- /dev/null +++ b/lib/FederatedItems/CircleSetting.php @@ -0,0 +1,118 @@ + + * @copyright 2022 + * @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 . + * + */ + + +namespace OCA\Circles\FederatedItems; + +use ArtificialOwl\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22Deserialize; +use OCA\Circles\Db\CircleRequest; +use OCA\Circles\IFederatedItem; +use OCA\Circles\IFederatedItemAsyncProcess; +use OCA\Circles\Model\Federated\FederatedEvent; +use OCA\Circles\Model\Helpers\MemberHelper; + +class CircleSetting implements + IFederatedItem, + IFederatedItemAsyncProcess { + use TNC22Deserialize; + + + /** @var CircleRequest */ + private $circleRequest; + + + /** + * CircleConfig constructor. + * + * @param CircleRequest $circleRequest + */ + public function __construct(CircleRequest $circleRequest) { + $this->circleRequest = $circleRequest; + } + + + /** + * @param FederatedEvent $event + */ + public function verify(FederatedEvent $event): void { + $circle = $event->getCircle(); + + $initiatorHelper = new MemberHelper($circle->getInitiator()); + $initiatorHelper->mustBeAdmin(); + + $params = $event->getParams(); + $setting = $params->g('setting'); + $value = $params->gBool('unset') ? null : $params->g('value'); + + $settings = $circle->getSettings(); + + if (!is_null($value)) { + $settings[$setting] = $value; + } elseif (array_key_exists($setting, $settings)) { + unset($settings[$setting]); + } + + $event->getData()->sArray('settings', $settings); + + $new = clone $circle; + $new->setSettings($settings); + + $event->setOutcome($this->serialize($new)); + } + + + /** + * @param FederatedEvent $event + */ + public function manage(FederatedEvent $event): void { + $circle = clone $event->getCircle(); + $settings = $event->getData()->gArray('settings'); + + $circle->setSettings($settings); + // TODO list imported from FederatedItem/CircleConfig.php - need to check first there. + + // TODO: Check locally that circle is not un-federated during the process + // TODO: if the circle is managed remotely, remove the circle locally + // TODO: if the circle is managed locally, remove non-local users + + // TODO: Check locally that circle is not federated during the process + // TODO: sync if it is to broadcast to Trusted RemoteInstance + + $this->circleRequest->updateSettings($circle); + } + + + /** + * @param FederatedEvent $event + * @param array $results + */ + public function result(FederatedEvent $event, array $results): void { + } +} diff --git a/lib/GlobalScale/FileShare.php b/lib/GlobalScale/FileShare.php index c3d7a8aec..2a2da0a37 100644 --- a/lib/GlobalScale/FileShare.php +++ b/lib/GlobalScale/FileShare.php @@ -198,14 +198,14 @@ private function sendShareToContact(GSEvent $event, DeprecatedCircle $circle, st $newCircle = $this->circlesRequest->forceGetCircle($circle->getUniqueId(), true); $password = ''; $sendPasswordByMail = true; - if ($this->configService->enforcePasswordProtection($newCircle)) { - if ($newCircle->getSetting('password_single_enabled') === 'true') { - $password = $newCircle->getPasswordSingle(); - $sendPasswordByMail = false; - } else { - $password = $this->miscService->token(15); - } - } +// if ($this->configService->enforcePasswordProtection($newCircle)) { +// if ($newCircle->getSetting('password_single_enabled') === 'true') { +// $password = $newCircle->getPasswordSingle(); +// $sendPasswordByMail = false; +// } else { +// $password = $this->miscService->token(15); +// } +// } try { $sharesToken = @@ -310,9 +310,9 @@ protected function sendMail($fileName, $link, $author, $circleName, $email) { * @throws Exception */ protected function sendPasswordByMail(IShare $share, $circleName, $email, $password) { - if (!$this->configService->sendPasswordByMail() || $password === '') { - return; - } +// if (!$this->configService->sendPasswordByMail() || $password === '') { +// return; +// } $message = $this->mailer->createMessage(); diff --git a/lib/GlobalScale/MemberAdd.php b/lib/GlobalScale/MemberAdd.php index 201cefec2..288f02f56 100644 --- a/lib/GlobalScale/MemberAdd.php +++ b/lib/GlobalScale/MemberAdd.php @@ -55,7 +55,7 @@ /** * Class MemberAdd - * + * @deprecated * @package OCA\Circles\GlobalScale */ class MemberAdd extends AGlobalScaleEvent { @@ -111,14 +111,14 @@ public function verify(GSEvent $event, bool $localCheck = false, bool $mustBeChe $password = ''; $sendPasswordByMail = false; - if ($this->configService->enforcePasswordProtection($circle)) { - if ($circle->getSetting('password_single_enabled') === 'true') { - $password = $circle->getPasswordSingle(); - } else { - $sendPasswordByMail = true; - $password = $this->miscService->token(15); - } - } +// if ($this->configService->enforcePasswordProtection($circle)) { +// if ($circle->getSetting('password_single_enabled') === 'true') { +// $password = $circle->getPasswordSingle(); +// } else { +// $sendPasswordByMail = true; +// $password = $this->miscService->token(15); +// } +// } $event->setData( new SimpleDataStore( diff --git a/lib/Listeners/Files/PreparingMemberSendMail.php b/lib/Listeners/Files/PreparingMemberSendMail.php index fc42626cb..b473984fd 100644 --- a/lib/Listeners/Files/PreparingMemberSendMail.php +++ b/lib/Listeners/Files/PreparingMemberSendMail.php @@ -44,11 +44,11 @@ use OCA\Circles\Model\Member; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\ContactService; +use OCA\Circles\Service\SendMailService; use OCA\Circles\Service\ShareTokenService; use OCA\Circles\Service\ShareWrapperService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; -use OCP\Security\IHasher; /** * Class PreparingMemberSendMail @@ -60,15 +60,15 @@ class PreparingMemberSendMail implements IEventListener { use TNC22Logger; - /** @var IHasher */ - private $hasher; - /** @var ShareWrapperService */ private $shareWrapperService; /** @var ShareTokenService */ private $shareTokenService; + /** @var SendMailService */ + private $sendMailService; + /** @var ConfigService */ private $configService; @@ -79,22 +79,22 @@ class PreparingMemberSendMail implements IEventListener { /** * AddingMember constructor. * - * @param IHasher $hasher * @param ShareWrapperService $shareWrapperService * @param ShareTokenService $shareTokenService + * @param SendMailService $sendMailService * @param ContactService $contactService * @param ConfigService $configService */ public function __construct( - IHasher $hasher, ShareWrapperService $shareWrapperService, ShareTokenService $shareTokenService, + SendMailService $sendMailService, ContactService $contactService, ConfigService $configService ) { - $this->hasher = $hasher; $this->shareWrapperService = $shareWrapperService; $this->shareTokenService = $shareTokenService; + $this->sendMailService = $sendMailService; $this->contactService = $contactService; $this->configService = $configService; @@ -113,8 +113,12 @@ public function __construct( * @throws UnknownRemoteException */ public function handle(Event $event): void { - if (!$event instanceof PreparingCircleMemberEvent - || !$this->configService->enforcePasswordOnSharedFile()) { + if (!$event instanceof PreparingCircleMemberEvent) { + return; + } + + $circle = $event->getCircle(); + if (!$this->configService->enforcePasswordOnSharedFile($circle)) { return; } @@ -126,9 +130,8 @@ public function handle(Event $event): void { } $federatedEvent = $event->getFederatedEvent(); - $clearPasswords = $federatedEvent->getInternal()->gArray('clearPasswords'); - $hashedPasswords = $federatedEvent->getParams()->gArray('hashedPasswords'); + $hashedPasswords = $clearPasswords = []; foreach ($members as $member) { if (($member->getUserType() !== Member::TYPE_MAIL && $member->getUserType() !== Member::TYPE_CONTACT) @@ -137,9 +140,9 @@ public function handle(Event $event): void { continue; } - $clearPassword = $this->token(14); + [$clearPassword, $hashedPassword] = $this->sendMailService->getPassword($circle); $clearPasswords[$member->getSingleId()] = $clearPassword; - $hashedPasswords[$member->getSingleId()] = $this->hasher->hash($clearPassword); + $hashedPasswords[$member->getSingleId()] = $hashedPassword; } $federatedEvent->getInternal()->aArray('clearPasswords', $clearPasswords); diff --git a/lib/Listeners/Files/PreparingShareSendMail.php b/lib/Listeners/Files/PreparingShareSendMail.php index 54def7b71..233349a60 100644 --- a/lib/Listeners/Files/PreparingShareSendMail.php +++ b/lib/Listeners/Files/PreparingShareSendMail.php @@ -44,6 +44,7 @@ use OCA\Circles\Model\Member; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\ContactService; +use OCA\Circles\Service\SendMailService; use OCA\Circles\Service\ShareTokenService; use OCA\Circles\Service\ShareWrapperService; use OCP\EventDispatcher\Event; @@ -69,6 +70,9 @@ class PreparingShareSendMail implements IEventListener { /** @var ShareTokenService */ private $shareTokenService; + /** @var SendMailService */ + private $sendMailService; + /** @var ConfigService */ private $configService; @@ -82,6 +86,7 @@ class PreparingShareSendMail implements IEventListener { * @param IHasher $hasher * @param ShareWrapperService $shareWrapperService * @param ShareTokenService $shareTokenService + * @param SendMailService $sendMailService * @param ContactService $contactService * @param ConfigService $configService */ @@ -89,12 +94,14 @@ public function __construct( IHasher $hasher, ShareWrapperService $shareWrapperService, ShareTokenService $shareTokenService, + SendMailService $sendMailService, ContactService $contactService, ConfigService $configService ) { $this->hasher = $hasher; $this->shareWrapperService = $shareWrapperService; $this->shareTokenService = $shareTokenService; + $this->sendMailService = $sendMailService; $this->contactService = $contactService; $this->configService = $configService; @@ -113,16 +120,18 @@ public function __construct( * @throws UnknownRemoteException */ public function handle(Event $event): void { - if (!$event instanceof PreparingFileShareEvent - || !$this->configService->enforcePasswordOnSharedFile()) { + if (!$event instanceof PreparingFileShareEvent) { return; } $circle = $event->getCircle(); + if (!$this->configService->enforcePasswordOnSharedFile($circle)) { + return; + } + $federatedEvent = $event->getFederatedEvent(); - $clearPasswords = $federatedEvent->getInternal()->gArray('clearPasswords'); - $hashedPasswords = $federatedEvent->getParams()->gArray('hashedPasswords'); + $hashedPasswords = $clearPasswords = []; foreach ($circle->getInheritedMembers(false, true) as $member) { if (($member->getUserType() !== Member::TYPE_MAIL && $member->getUserType() !== Member::TYPE_CONTACT) @@ -132,9 +141,9 @@ public function handle(Event $event): void { continue; } - $clearPassword = $this->token(14); + [$clearPassword, $hashedPassword] = $this->sendMailService->getPassword($circle); $clearPasswords[$member->getSingleId()] = $clearPassword; - $hashedPasswords[$member->getSingleId()] = $this->hasher->hash($clearPassword); + $hashedPasswords[$member->getSingleId()] = $hashedPassword; } $federatedEvent->getInternal()->aArray('clearPasswords', $clearPasswords); diff --git a/lib/Model/Circle.php b/lib/Model/Circle.php index fe5cdb169..5a6cdb6b1 100644 --- a/lib/Model/Circle.php +++ b/lib/Model/Circle.php @@ -38,6 +38,7 @@ use ArtificialOwl\MySmallPhpTools\Traits\TArrayTools; use DateTime; use JsonSerializable; +use OCA\Circles\Db\CircleRequest; use OCA\Circles\Exceptions\CircleNotFoundException; use OCA\Circles\Exceptions\FederatedItemException; use OCA\Circles\Exceptions\OwnerNotFoundException; @@ -781,6 +782,12 @@ public function import(array $data): IDeserializable { /** * @return array + * @throws FederatedItemException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException + * @throws UnknownRemoteException */ public function jsonSerialize(): array { $arr = [ @@ -792,7 +799,6 @@ public function jsonSerialize(): array { 'population' => $this->getPopulation(), 'config' => $this->getConfig(), 'description' => $this->getDescription(), - 'settings' => $this->getSettings(), 'url' => $this->getUrl(), 'creation' => $this->getCreation(), 'initiator' => ($this->hasInitiator()) ? $this->getInitiator() : null @@ -814,6 +820,16 @@ public function jsonSerialize(): array { $arr['memberships'] = $this->getMemberships(); } + // settings should only be available to admin + if ($this->hasInitiator()) { + $initiatorHelper = new MemberHelper($this->getInitiator()); + try { + $initiatorHelper->mustBeAdmin(); + $arr['settings'] = $this->getSettings(); + } catch (MemberHelperException | MemberLevelException $e) { + } + } + return $arr; } @@ -848,6 +864,26 @@ public function importFromDatabase(array $data, string $prefix = ''): INC22Query $this->getManager()->manageImportFromDatabase($this, $data, $prefix); + + // TODO: deprecated in NC27, remove those (17) lines that was needed to finalise migration to 24 + // if password is not hashed (pre-22), hash it and update new settings in DB + $curr = $this->get('password_single', $this->getSettings()); + if (strlen($curr) >= 1 && strlen($curr) < 64) { + /** @var IHasher $hasher */ + $hasher = \OC::$server->get(IHasher::class); + /** @var CircleRequest $circleRequest */ + $circleRequest = \OC::$server->get(CircleRequest::class); + + $new = $hasher->hash($curr); + $settings = $this->getSettings(); + $settings['password_single'] = $new; + $this->setSettings($settings); + + $circleRequest->updateSettings($this); + } + + // END deprecated NC27 + return $this; } diff --git a/lib/Service/CircleService.php b/lib/Service/CircleService.php index 45a60c385..275af25b4 100644 --- a/lib/Service/CircleService.php +++ b/lib/Service/CircleService.php @@ -67,12 +67,8 @@ use OCA\Circles\Model\Probes\MemberProbe; use OCA\Circles\StatusCode; use OCP\IL10N; +use OCP\Security\IHasher; -/** - * Class CircleService - * - * @package OCA\Circles\Service - */ class CircleService { use TArrayTools; use TStringTools; @@ -82,6 +78,9 @@ class CircleService { /** @var IL10N */ private $l10n; + /** @var IHasher */ + private $hasher; + /** @var CircleRequest */ private $circleRequest; @@ -105,8 +104,8 @@ class CircleService { /** - * CircleService constructor. - * + * @param IL10N $l10n + * @param IHasher $hasher * @param CircleRequest $circleRequest * @param MemberRequest $memberRequest * @param RemoteStreamService $remoteStreamService @@ -117,6 +116,7 @@ class CircleService { */ public function __construct( IL10N $l10n, + IHasher $hasher, CircleRequest $circleRequest, MemberRequest $memberRequest, RemoteStreamService $remoteStreamService, @@ -126,6 +126,7 @@ public function __construct( ConfigService $configService ) { $this->l10n = $l10n; + $this->hasher = $hasher; $this->circleRequest = $circleRequest; $this->memberRequest = $memberRequest; $this->remoteStreamService = $remoteStreamService; @@ -272,8 +273,11 @@ public function updateConfig(string $circleId, int $config): array { /** + * if $value is null, setting is unset + * * @param string $circleId - * @param string $name + * @param string $setting + * @param string|null $value * * @return array * @throws CircleNotFoundException @@ -288,21 +292,34 @@ public function updateConfig(string $circleId, int $config): array { * @throws RequestBuilderException * @throws UnknownRemoteException */ - public function updateName(string $circleId, string $name): array { + public function updateSetting(string $circleId, string $setting, ?string $value): array { $circle = $this->getCircle($circleId); - $event = new FederatedEvent(CircleEdit::class); + if (strtolower($setting) === 'password_single' && !is_null($value)) { + $value = $this->hasher->hash($value); + } + + $event = new FederatedEvent(CircleSetting::class); $event->setCircle($circle); - $event->setParams(new SimpleDataStore(['name' => $name])); + $event->setParams( + new SimpleDataStore( + [ + 'setting' => $setting, + 'value' => $value, + 'unset' => is_null($value) + ] + ) + ); $this->federatedEventService->newEvent($event); return $event->getOutcome(); } + /** * @param string $circleId - * @param string $description + * @param string $name * * @return array * @throws CircleNotFoundException @@ -317,12 +334,12 @@ public function updateName(string $circleId, string $name): array { * @throws RequestBuilderException * @throws UnknownRemoteException */ - public function updateDescription(string $circleId, string $description): array { + public function updateName(string $circleId, string $name): array { $circle = $this->getCircle($circleId); $event = new FederatedEvent(CircleEdit::class); $event->setCircle($circle); - $event->setParams(new SimpleDataStore(['description' => $description])); + $event->setParams(new SimpleDataStore(['name' => $name])); $this->federatedEventService->newEvent($event); @@ -331,7 +348,7 @@ public function updateDescription(string $circleId, string $description): array /** * @param string $circleId - * @param array $settings + * @param string $description * * @return array * @throws CircleNotFoundException @@ -346,12 +363,12 @@ public function updateDescription(string $circleId, string $description): array * @throws RequestBuilderException * @throws UnknownRemoteException */ - public function updateSettings(string $circleId, array $settings): array { + public function updateDescription(string $circleId, string $description): array { $circle = $this->getCircle($circleId); - $event = new FederatedEvent(CircleSettings::class); + $event = new FederatedEvent(CircleEdit::class); $event->setCircle($circle); - $event->setParams(new SimpleDataStore(['settings' => $settings])); + $event->setParams(new SimpleDataStore(['description' => $description])); $this->federatedEventService->newEvent($event); diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 0ff4e8dac..9a21f324c 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -40,7 +40,6 @@ use OCA\Circles\Exceptions\GSStatusException; use OCA\Circles\IFederatedUser; use OCA\Circles\Model\Circle; -use OCA\Circles\Model\DeprecatedCircle; use OCA\Circles\Model\Member; use OCP\IConfig; use OCP\IURLGenerator; @@ -331,56 +330,52 @@ public function contactsBackendType(): int { /** + * true if: + * - password is generated randomly + * + * @param Circle $circle + * * @return bool - * @deprecated - * should the password for a mail share be send to the recipient */ - public function sendPasswordByMail(): bool { - return false; + public function sendPasswordByMail(Circle $circle): bool { + if (!$this->enforcePasswordOnSharedFile($circle)) { + return false; + } + + return (!$this->getBool('password_single_enabled', $circle->getSettings(), false) + || $this->get('password_single', $circle->getSettings()) === ''); } /** + * true if: + * - global setting of Nextcloud enforce password on shares. + * - setting of Circles' app enforce password on shares. + * - setting for specific Circle enforce password on shares. + * + * @param Circle $circle + * * @return bool */ - public function enforcePasswordOnSharedFile(): bool { - $localPolicy = $this->getAppValueInt(ConfigService::ENFORCE_PASSWORD); - if ($localPolicy !== $this->getInt(ConfigService::ENFORCE_PASSWORD, self::$defaults)) { - return ($localPolicy === 1); + public function enforcePasswordOnSharedFile(Circle $circle): bool { + if ($this->config->getAppValue( + 'core', + 'shareapi_enforce_links_password', + 'no' + ) === 'yes') { + return true; } - // TODO: reimplement a way to set password protection on a single Circle -// if ($circle->getSetting('password_enforcement') === 'true') { -// return true; -// } - - $sendPasswordMail = $this->config->getAppValue( - 'sharebymail', - 'sendpasswordmail', - 'yes' - ); - - $enforcePasswordProtection = $this->config->getAppValue( - 'core', - 'shareapi_enforce_links_password', - 'no' - ); - - return ($sendPasswordMail === 'yes' - && $enforcePasswordProtection === 'yes'); - } + if ($this->getAppValueInt(ConfigService::ENFORCE_PASSWORD) === 1) { + return true; + } + // Compat NC21 + if ($this->getBool('password_enforcement', $circle->getSettings(), false)) { + return true; + } - /** - * @param DeprecatedCircle $circle - * - * @return bool - * @deprecated - * do we require a share by mail to be password protected - * - */ - public function enforcePasswordProtection(DeprecatedCircle $circle) { - return false; + return $this->getBool('enforce_password', $circle->getSettings(), false); } diff --git a/lib/Service/SendMailService.php b/lib/Service/SendMailService.php index 8506d8a99..7c3305b35 100644 --- a/lib/Service/SendMailService.php +++ b/lib/Service/SendMailService.php @@ -31,6 +31,8 @@ namespace OCA\Circles\Service; +use ArtificialOwl\MySmallPhpTools\Traits\TArrayTools; +use ArtificialOwl\MySmallPhpTools\Traits\TStringTools; use Exception; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Member; @@ -39,19 +41,20 @@ use OCP\IL10N; use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; +use OCP\Security\IHasher; use OCP\Util; -/** - * Class SendMailService - * - * @package OCA\Circles\Service - */ class SendMailService { + use TArrayTools; + use TStringTools; /** @var IL10N */ private $l10n; + /** @var IHasher */ + private $hasher; + /** @var IMailer */ private $mailer; @@ -72,11 +75,13 @@ class SendMailService { */ public function __construct( IL10N $l10n, + IHasher $hasher, IMailer $mailer, Defaults $defaults, ConfigService $configService ) { $this->l10n = $l10n; + $this->hasher = $hasher; $this->mailer = $mailer; $this->defaults = $defaults; $this->configService = $configService; @@ -132,7 +137,7 @@ public function generateMail( } catch (Exception $e) { } - $this->sendMailPassword($author, $circle->getDisplayName(), $mail, $password); + $this->sendMailPassword($circle, $author, $mail, $password); } } @@ -213,18 +218,20 @@ private function sendMailExistingShares( /** + * @param Circle $circle * @param string $author - * @param string $circleName * @param string $email * @param string $password + * + * @throws Exception */ private function sendMailPassword( + Circle $circle, string $author, - string $circleName, string $email, string $password ): void { - if (!$this->configService->enforcePasswordOnSharedFile() || $password === '') { + if (!$this->configService->sendPasswordByMail($circle) || $password === '') { return; } @@ -246,7 +253,7 @@ private function sendMailPassword( 'initiator' => $author, // 'initiatorEmail' => Util::getDefaultEmailAddress(''), 'initiatorEmail' => '', - 'shareWith' => $circleName + 'shareWith' => $circle->getDisplayName() ] ); @@ -280,4 +287,23 @@ private function sendMailPassword( $message->useTemplate($emailTemplate); $this->mailer->send($message); } + + + /** + * @param Circle $circle + * + * @return array + */ + public function getPassword(Circle $circle): array { + $clearPassword = $hashedPassword = ''; + if (!$this->configService->sendPasswordByMail($circle)) { + $hashedPassword = $this->get('password_single', $circle->getSettings()); + } + if ($hashedPassword === '') { + $clearPassword = $this->token(14); + $hashedPassword = $this->hasher->hash($clearPassword); + } + + return [$clearPassword, $hashedPassword]; + } } From 400f47a60f234c5a95ee46cb9aa95fad16eaae4c Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Wed, 9 Mar 2022 10:06:01 -0100 Subject: [PATCH 2/2] missing import Signed-off-by: Maxence Lange --- lib/Model/Circle.php | 4 ++++ lib/Service/CircleService.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Model/Circle.php b/lib/Model/Circle.php index 5a6cdb6b1..9f968434d 100644 --- a/lib/Model/Circle.php +++ b/lib/Model/Circle.php @@ -41,6 +41,8 @@ use OCA\Circles\Db\CircleRequest; use OCA\Circles\Exceptions\CircleNotFoundException; use OCA\Circles\Exceptions\FederatedItemException; +use OCA\Circles\Exceptions\MemberHelperException; +use OCA\Circles\Exceptions\MemberLevelException; use OCA\Circles\Exceptions\OwnerNotFoundException; use OCA\Circles\Exceptions\RemoteInstanceException; use OCA\Circles\Exceptions\RemoteNotFoundException; @@ -48,6 +50,8 @@ use OCA\Circles\Exceptions\RequestBuilderException; use OCA\Circles\Exceptions\UnknownRemoteException; use OCA\Circles\IMemberships; +use OCA\Circles\Model\Helpers\MemberHelper; +use OCP\Security\IHasher; /** * Class Circle diff --git a/lib/Service/CircleService.php b/lib/Service/CircleService.php index 275af25b4..3eb225004 100644 --- a/lib/Service/CircleService.php +++ b/lib/Service/CircleService.php @@ -57,7 +57,7 @@ use OCA\Circles\FederatedItems\CircleEdit; use OCA\Circles\FederatedItems\CircleJoin; use OCA\Circles\FederatedItems\CircleLeave; -use OCA\Circles\FederatedItems\CircleSettings; +use OCA\Circles\FederatedItems\CircleSetting; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Federated\FederatedEvent; use OCA\Circles\Model\FederatedUser;