From 5c2eb732f123c907201a2db36f12f0a269722ce7 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Thu, 11 May 2023 18:59:30 +0200 Subject: [PATCH] fix(carddav): expose system address book Signed-off-by: Anna Larch --- apps/dav/appinfo/v1/carddav.php | 3 +- apps/dav/lib/CardDAV/AddressBookRoot.php | 13 ++- apps/dav/lib/CardDAV/CardDavBackend.php | 5 ++ apps/dav/lib/CardDAV/SyncService.php | 18 +++-- apps/dav/lib/CardDAV/SystemAddressbook.php | 79 ++++++++++++++++++- apps/dav/lib/CardDAV/UserAddressBooks.php | 53 ++++++++++--- apps/dav/lib/RootCollection.php | 5 +- .../unit/CardDAV/SystemAddressBookTest.php | 5 ++ .../templates/settings/admin/sharing.php | 4 +- 9 files changed, 158 insertions(+), 27 deletions(-) diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index 2b3001e80bd..e7faa9314e2 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -10,6 +10,7 @@ * @author Morris Jobke * @author Thomas Citharel * @author Thomas Müller + * @author Anna Larch * * @license AGPL-3.0 * @@ -72,7 +73,7 @@ $principalCollection = new \Sabre\CalDAV\Principal\Collection($principalBackend) $principalCollection->disableListing = !$debugging; // Disable listing $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); -$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend, $pluginManager); +$addressBookRoot = new AddressBookRoot($principalBackend, $cardDavBackend, $pluginManager, \OC::$server->getUserSession()->getUser(), \OC::$server->get(\OCP\IGroupManager::class)); $addressBookRoot->disableListing = !$debugging; // Disable listing $nodes = [ diff --git a/apps/dav/lib/CardDAV/AddressBookRoot.php b/apps/dav/lib/CardDAV/AddressBookRoot.php index 897ed819071..c82943d2879 100644 --- a/apps/dav/lib/CardDAV/AddressBookRoot.php +++ b/apps/dav/lib/CardDAV/AddressBookRoot.php @@ -5,6 +5,7 @@ * @author Christoph Wurst * @author Joas Schilling * @author Thomas Müller + * @author Anna Larch * * @license AGPL-3.0 * @@ -24,11 +25,15 @@ namespace OCA\DAV\CardDAV; use OCA\DAV\AppInfo\PluginManager; +use OCP\IGroupManager; +use OCP\IUser; class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { /** @var PluginManager */ private $pluginManager; + private ?IUser $user; + private ?IGroupManager $groupManager; /** * @param \Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend @@ -38,9 +43,13 @@ class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend, \Sabre\CardDAV\Backend\BackendInterface $carddavBackend, PluginManager $pluginManager, - $principalPrefix = 'principals') { + ?IUser $user, + ?IGroupManager $groupManager, + string $principalPrefix = 'principals') { parent::__construct($principalBackend, $carddavBackend, $principalPrefix); $this->pluginManager = $pluginManager; + $this->user = $user; + $this->groupManager = $groupManager; } /** @@ -55,7 +64,7 @@ class AddressBookRoot extends \Sabre\CardDAV\AddressBookRoot { * @return \Sabre\DAV\INode */ public function getChildForPrincipal(array $principal) { - return new UserAddressBooks($this->carddavBackend, $principal['uri'], $this->pluginManager); + return new UserAddressBooks($this->carddavBackend, $principal['uri'], $this->pluginManager, $this->user, $this->groupManager); } public function getName() { diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 577d7282eae..5f5b8f1e65f 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -311,6 +311,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0', ]; + // system address books are always read only + if ($principal === 'principals/system/system') { + $addressBook['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'] = true; + } + $this->addOwnerPrincipal($addressBook); return $addressBook; diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index da798c5768e..b228d45f067 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -10,6 +10,7 @@ * @author Morris Jobke * @author Thomas Citharel * @author Thomas Müller + * @author Anna Larch * * @license AGPL-3.0 * @@ -209,10 +210,8 @@ class SyncService { public function updateUser(IUser $user) { $systemAddressBook = $this->getLocalSystemAddressBook(); $addressBookId = $systemAddressBook['id']; - $name = $user->getBackendClassName(); - $userId = $user->getUID(); - $cardId = "$name:$userId.vcf"; + $cardId = self::getCardUri($user); if ($user->isEnabled()) { $card = $this->backend->getCard($addressBookId, $cardId); if ($card === false) { @@ -239,10 +238,7 @@ class SyncService { public function deleteUser($userOrCardId) { $systemAddressBook = $this->getLocalSystemAddressBook(); if ($userOrCardId instanceof IUser) { - $name = $userOrCardId->getBackendClassName(); - $userId = $userOrCardId->getUID(); - - $userOrCardId = "$name:$userId.vcf"; + $userOrCardId = self::getCardUri($userOrCardId); } $this->backend->deleteCard($systemAddressBook['id'], $userOrCardId); } @@ -281,4 +277,12 @@ class SyncService { } } } + + /** + * @param IUser $user + * @return string + */ + public static function getCardUri(IUser $user): string { + return $user->getBackendClassName() . ':' . $user->getUID() . '.vcf'; + } } diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index a803a1e6b24..17900fd033e 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -8,6 +8,7 @@ declare(strict_types=1); * @author Joas Schilling * @author Julius Härtl * @author Roeland Jago Douma + * @author Anna Larch * * @license GNU AGPL version 3 or any later version * @@ -31,38 +32,97 @@ use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCA\Federation\TrustedServers; use OCP\Accounts\IAccountManager; use OCP\IConfig; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; use Sabre\CardDAV\Backend\SyncSupport; use Sabre\CardDAV\Backend\BackendInterface; use Sabre\CardDAV\Card; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; use Sabre\VObject\Component\VCard; use Sabre\VObject\Reader; +use function array_unique; class SystemAddressbook extends AddressBook { + public const URI_SHARED = 'z-server-generated--system'; /** @var IConfig */ private $config; + private IUserSession $userSession; private ?TrustedServers $trustedServers; private ?IRequest $request; + private ?IGroupManager $groupManager; - public function __construct(BackendInterface $carddavBackend, array $addressBookInfo, IL10N $l10n, IConfig $config, ?IRequest $request = null, ?TrustedServers $trustedServers = null) { + public function __construct(BackendInterface $carddavBackend, + array $addressBookInfo, + IL10N $l10n, + IConfig $config, + IUserSession $userSession, + ?IRequest $request = null, + ?TrustedServers $trustedServers = null, + ?IGroupManager $groupManager) { parent::__construct($carddavBackend, $addressBookInfo, $l10n); $this->config = $config; + $this->userSession = $userSession; $this->request = $request; $this->trustedServers = $trustedServers; + $this->groupManager = $groupManager; + + $this->addressBookInfo['{DAV:}displayname'] = $l10n->t('Accounts'); + $this->addressBookInfo['{' . Plugin::NS_CARDDAV . '}addressbook-description'] = $l10n->t('System address book which holds all accounts'); } - public function getChildren(): array { + /** + * No checkbox checked -> Show only the same user + * 'Allow username autocompletion in share dialog' -> show everyone + * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' -> show only users in intersecting groups + * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users based on phone number integration' -> show only the same user + * 'Allow username autocompletion in share dialog' + 'Allow username autocompletion to users within the same groups' + 'Allow username autocompletion to users based on phone number integration' -> show only users in intersecting groups + */ + public function getChildren() { $shareEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $shareEnumerationGroup = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $shareEnumerationPhone = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; - if (!$shareEnumeration || $shareEnumerationGroup || $shareEnumerationPhone) { + $user = $this->userSession->getUser(); + if (!$user) { + // Should never happen because we don't allow anonymous access return []; } + if (!$shareEnumeration || !$shareEnumerationGroup && $shareEnumerationPhone) { + $name = SyncService::getCardUri($user); + try { + return [parent::getChild($name)]; + } catch (NotFound $e) { + return []; + } + } + if ($shareEnumerationGroup) { + if ($this->groupManager === null) { + // Group manager is not available, so we can't determine which data is safe + return []; + } + $groups = $this->groupManager->getUserGroups($user); + $names = []; + foreach ($groups as $group) { + $users = $group->getUsers(); + foreach ($users as $groupUser) { + if ($groupUser->getBackendClassName() === 'Guests') { + continue; + } + $names[] = SyncService::getCardUri($groupUser); + } + } + return parent::getMultipleChildren(array_unique($names)); + } - return parent::getChildren(); + $children = parent::getChildren(); + return array_filter($children, function (Card $child) { + // check only for URIs that begin with Guests: + return strpos($child->getName(), 'Guests:') !== 0; + }); } /** @@ -225,4 +285,15 @@ class SystemAddressbook extends AddressBook { return $vCard->serialize(); } + + /** + * @return mixed + * @throws Forbidden + */ + public function delete() { + if ($this->isFederation()) { + parent::delete(); + } + throw new Forbidden(); + } } diff --git a/apps/dav/lib/CardDAV/UserAddressBooks.php b/apps/dav/lib/CardDAV/UserAddressBooks.php index 85795604f28..938575bd1a7 100644 --- a/apps/dav/lib/CardDAV/UserAddressBooks.php +++ b/apps/dav/lib/CardDAV/UserAddressBooks.php @@ -9,6 +9,7 @@ declare(strict_types=1); * @author Joas Schilling * @author Roeland Jago Douma * @author Thomas Müller + * @author Anna Larch * * @license AGPL-3.0 * @@ -33,8 +34,11 @@ use OCA\DAV\CardDAV\Integration\ExternalAddressBook; use OCA\Federation\TrustedServers; use OCP\AppFramework\QueryException; use OCP\IConfig; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Sabre\CardDAV\Backend; @@ -44,7 +48,6 @@ use function array_map; use Sabre\DAV\MkCol; class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { - /** @var IL10N */ protected $l10n; @@ -53,12 +56,18 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { /** @var PluginManager */ private $pluginManager; + private ?IUser $user; + private ?IGroupManager $groupManager; public function __construct(Backend\BackendInterface $carddavBackend, string $principalUri, - PluginManager $pluginManager) { + PluginManager $pluginManager, + ?IUser $user, + ?IGroupManager $groupManager) { parent::__construct($carddavBackend, $principalUri); $this->pluginManager = $pluginManager; + $this->user = $user; + $this->groupManager = $groupManager; } /** @@ -74,10 +83,25 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { $this->config = \OC::$server->getConfig(); } + /** @var string|array $principal */ + $principal = $this->principalUri; $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); - /** @var IAddressBook[] $objects */ - $objects = array_map(function (array $addressBook) { - if ($addressBook['principaluri'] === 'principals/system/system') { + // add the system address book + $systemAddressBook = null; + if (is_string($principal) && $principal !== 'principals/system/system' && $this->carddavBackend instanceof CardDavBackend) { + $systemAddressBook = $this->carddavBackend->getAddressBooksByUri('principals/system/system', 'system'); + if ($systemAddressBook !== null) { + $systemAddressBook['uri'] = SystemAddressbook::URI_SHARED; + } + } + if (!is_null($systemAddressBook)) { + $addressBooks[] = $systemAddressBook; + } + + $objects = []; + if (!empty($addressBooks)) { + /** @var IAddressBook[] $objects */ + $objects = array_map(function (array $addressBook) { $trustedServers = null; $request = null; try { @@ -86,11 +110,22 @@ class UserAddressBooks extends \Sabre\CardDAV\AddressBookHome { } catch (NotFoundExceptionInterface | ContainerExceptionInterface $e) { // nothing to do, the request / trusted servers don't exist } - return new SystemAddressbook($this->carddavBackend, $addressBook, $this->l10n, $this->config, $request, $trustedServers); - } + if ($addressBook['principaluri'] === 'principals/system/system') { + return new SystemAddressbook( + $this->carddavBackend, + $addressBook, + $this->l10n, + $this->config, + \OCP\Server::get(IUserSession::class), + $request, + $trustedServers, + $this->groupManager + ); + } - return new AddressBook($this->carddavBackend, $addressBook, $this->l10n); - }, $addressBooks); + return new AddressBook($this->carddavBackend, $addressBook, $this->l10n); + }, $addressBooks); + } /** @var IAddressBook[][] $objectsFromPlugins */ $objectsFromPlugins = array_map(function (IAddressBookProvider $plugin): array { return $plugin->fetchAllForAddressBookHome($this->principalUri); diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 6e009875ddc..80d96f0d748 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -50,6 +50,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\IConfig; +use OCP\IGroupManager; use Psr\Log\LoggerInterface; use Sabre\DAV\SimpleCollection; @@ -144,11 +145,11 @@ class RootCollection extends SimpleCollection { $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); - $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, 'principals/users'); + $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); - $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, 'principals/system'); + $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; $uploadCollection = new Upload\RootCollection( diff --git a/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php index 73393d75615..a753a1c5a73 100644 --- a/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php +++ b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php @@ -32,6 +32,7 @@ use OCP\Accounts\IAccountManager; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; +use OCP\IUserSession; use PHPUnit\Framework\MockObject\MockObject; use Sabre\CardDAV\Backend\BackendInterface; use Sabre\VObject\Component\VCard; @@ -44,6 +45,7 @@ class SystemAddressBookTest extends TestCase { private array $addressBookInfo; private IL10N|MockObject $l10n; private IConfig|MockObject $config; + private IUserSession $userSession; private IRequest|MockObject $request; private array $server; private TrustedServers|MockObject $trustedServers; @@ -60,6 +62,7 @@ class SystemAddressBookTest extends TestCase { ]; $this->l10n = $this->createMock(IL10N::class); $this->config = $this->createMock(IConfig::class); + $this->userSession = $this->createMock(IUserSession::class); $this->request = $this->createMock(Request::class); $this->server = [ 'PHP_AUTH_USER' => 'system', @@ -73,8 +76,10 @@ class SystemAddressBookTest extends TestCase { $this->addressBookInfo, $this->l10n, $this->config, + $this->userSession, $this->request, $this->trustedServers, + null, ); } diff --git a/apps/settings/templates/settings/admin/sharing.php b/apps/settings/templates/settings/admin/sharing.php index 781fdf3a49d..709c36a7b7b 100644 --- a/apps/settings/templates/settings/admin/sharing.php +++ b/apps/settings/templates/settings/admin/sharing.php @@ -209,7 +209,7 @@ /> -
+