Skip to content

Commit 7eac8a9

Browse files
committed
fix(dav): restrict calendar and address book sharing to the owner
Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
1 parent e492da8 commit 7eac8a9

14 files changed

Lines changed: 193 additions & 84 deletions

File tree

‎apps/dav/composer/composer/autoload_classmap.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
'OCA\\DAV\\DAV\\Sharing\\IShareable' => $baseDir . '/../lib/DAV/Sharing/IShareable.php',
242242
'OCA\\DAV\\DAV\\Sharing\\Plugin' => $baseDir . '/../lib/DAV/Sharing/Plugin.php',
243243
'OCA\\DAV\\DAV\\Sharing\\SharingMapper' => $baseDir . '/../lib/DAV/Sharing/SharingMapper.php',
244+
'OCA\\DAV\\DAV\\Sharing\\SharingPrivilegeSetTrait' => $baseDir . '/../lib/DAV/Sharing/SharingPrivilegeSetTrait.php',
244245
'OCA\\DAV\\DAV\\Sharing\\SharingService' => $baseDir . '/../lib/DAV/Sharing/SharingService.php',
245246
'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => $baseDir . '/../lib/DAV/Sharing/Xml/Invite.php',
246247
'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => $baseDir . '/../lib/DAV/Sharing/Xml/ShareRequest.php',

‎apps/dav/composer/composer/autoload_static.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class ComposerStaticInitDAV
256256
'OCA\\DAV\\DAV\\Sharing\\IShareable' => __DIR__ . '/..' . '/../lib/DAV/Sharing/IShareable.php',
257257
'OCA\\DAV\\DAV\\Sharing\\Plugin' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Plugin.php',
258258
'OCA\\DAV\\DAV\\Sharing\\SharingMapper' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingMapper.php',
259+
'OCA\\DAV\\DAV\\Sharing\\SharingPrivilegeSetTrait' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingPrivilegeSetTrait.php',
259260
'OCA\\DAV\\DAV\\Sharing\\SharingService' => __DIR__ . '/..' . '/../lib/DAV/Sharing/SharingService.php',
260261
'OCA\\DAV\\DAV\\Sharing\\Xml\\Invite' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/Invite.php',
261262
'OCA\\DAV\\DAV\\Sharing\\Xml\\ShareRequest' => __DIR__ . '/..' . '/../lib/DAV/Sharing/Xml/ShareRequest.php',

‎apps/dav/lib/CalDAV/Calendar.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use DateTimeInterface;
1313
use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin;
1414
use OCA\DAV\DAV\Sharing\IShareable;
15+
use OCA\DAV\DAV\Sharing\SharingPrivilegeSetTrait;
1516
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
1617
use OCP\DB\Exception;
1718
use OCP\IConfig;
@@ -31,6 +32,8 @@
3132
* @property CalDavBackend $caldavBackend
3233
*/
3334
class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
35+
use SharingPrivilegeSetTrait;
36+
3437
protected IL10N $l10n;
3538
private bool $useTrashbin = true;
3639

@@ -136,6 +139,11 @@ public function getACL() {
136139
'principal' => $this->getOwner() . '/calendar-proxy-write',
137140
'protected' => true,
138141
];
142+
$acl[] = [
143+
'privilege' => '{DAV:}write-acl',
144+
'principal' => $this->getOwner(),
145+
'protected' => true,
146+
];
139147
} else {
140148
$acl[] = [
141149
'privilege' => '{DAV:}write-properties',

‎apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ public function __construct(bool $public = true) {
8484
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
8585
//$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
8686
$this->server->addPlugin(new PublishPlugin(
87-
\OC::$server->getConfig(),
8887
\OC::$server->getURLGenerator()
8988
));
9089

‎apps/dav/lib/CalDAV/Publishing/PublishPlugin.php‎

Lines changed: 13 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use OCA\DAV\CalDAV\Calendar;
99
use OCA\DAV\CalDAV\Publishing\Xml\Publisher;
1010
use OCP\AppFramework\Http;
11-
use OCP\IConfig;
1211
use OCP\IURLGenerator;
1312
use Sabre\CalDAV\Xml\Property\AllowedSharingModes;
1413
use Sabre\DAV\Exception\NotFound;
@@ -29,20 +28,7 @@ class PublishPlugin extends ServerPlugin {
2928
*/
3029
protected $server;
3130

32-
/**
33-
* PublishPlugin constructor.
34-
*
35-
* @param IConfig $config
36-
* @param IURLGenerator $urlGenerator
37-
*/
3831
public function __construct(
39-
/**
40-
* Config instance to get instance secret.
41-
*/
42-
protected IConfig $config,
43-
/**
44-
* URL Generator for absolute URLs.
45-
*/
4632
protected IURLGenerator $urlGenerator,
4733
) {
4834
}
@@ -101,16 +87,10 @@ public function propFind(PropFind $propFind, INode $node) {
10187
}
10288
});
10389

104-
$propFind->handle('{' . self::NS_CALENDARSERVER . '}allowed-sharing-modes', function () use ($node) {
105-
$canShare = (!$node->isSubscription() && $node->canWrite());
106-
$canPublish = (!$node->isSubscription() && $node->canWrite());
90+
$propFind->handle('{' . self::NS_CALENDARSERVER . '}allowed-sharing-modes', function () use ($propFind, $node) {
91+
$canShare = !$node->isSubscription() && $this->canChangeSharing($propFind->getPath());
10792

108-
if ($this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes') {
109-
$canShare = $canShare && ($node->getOwner() === $node->getPrincipalURI());
110-
$canPublish = $canPublish && ($node->getOwner() === $node->getPrincipalURI());
111-
}
112-
113-
return new AllowedSharingModes($canShare, $canPublish);
93+
return new AllowedSharingModes($canShare, $canShare);
11494
});
11595
}
11696
}
@@ -162,20 +142,7 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
162142
}
163143
$this->server->transactionType = 'post-publish-calendar';
164144

165-
// Getting ACL info
166-
$acl = $this->server->getPlugin('acl');
167-
168-
// If there's no ACL support, we allow everything
169-
if ($acl) {
170-
/** @var \Sabre\DAVACL\Plugin $acl */
171-
$acl->checkPrivileges($path, '{DAV:}write');
172-
173-
$limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
174-
$isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
175-
if ($limitSharingToOwner && !$isOwner) {
176-
return;
177-
}
178-
}
145+
$this->canChangeSharing($path, true);
179146

180147
$node->setPublishStatus(true);
181148

@@ -197,20 +164,7 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
197164
}
198165
$this->server->transactionType = 'post-unpublish-calendar';
199166

200-
// Getting ACL info
201-
$acl = $this->server->getPlugin('acl');
202-
203-
// If there's no ACL support, we allow everything
204-
if ($acl) {
205-
/** @var \Sabre\DAVACL\Plugin $acl */
206-
$acl->checkPrivileges($path, '{DAV:}write');
207-
208-
$limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
209-
$isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
210-
if ($limitSharingToOwner && !$isOwner) {
211-
return;
212-
}
213-
}
167+
$this->canChangeSharing($path, true);
214168

215169
$node->setPublishStatus(false);
216170

@@ -225,4 +179,12 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
225179

226180
}
227181
}
182+
183+
private function canChangeSharing(string $path, bool $throwExceptions = false): bool {
184+
$acl = $this->server->getPlugin('acl');
185+
if (!$acl instanceof \Sabre\DAVACL\Plugin) {
186+
return true;
187+
}
188+
return $acl->checkPrivileges($path, '{DAV:}write-acl', \Sabre\DAVACL\Plugin::R_PARENT, $throwExceptions);
189+
}
228190
}

‎apps/dav/lib/CardDAV/AddressBook.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OCA\DAV\CardDAV;
99

1010
use OCA\DAV\DAV\Sharing\IShareable;
11+
use OCA\DAV\DAV\Sharing\SharingPrivilegeSetTrait;
1112
use OCP\DB\Exception;
1213
use OCP\IL10N;
1314
use OCP\Server;
@@ -26,6 +27,8 @@
2627
* @property CardDavBackend $carddavBackend
2728
*/
2829
class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMoveTarget {
30+
use SharingPrivilegeSetTrait;
31+
2932
/**
3033
* AddressBook constructor.
3134
*
@@ -96,6 +99,11 @@ public function getACL() {
9699
'principal' => $this->getOwner(),
97100
'protected' => true,
98101
],
102+
[
103+
'privilege' => '{DAV:}write-acl',
104+
'principal' => $this->getOwner(),
105+
'protected' => true,
106+
],
99107
[
100108
'privilege' => '{DAV:}write-properties',
101109
'principal' => $this->getOwner(),

‎apps/dav/lib/DAV/Sharing/Plugin.php‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use OCA\DAV\DAV\Sharing\Xml\Invite;
1414
use OCA\DAV\DAV\Sharing\Xml\ShareRequest;
1515
use OCP\AppFramework\Http;
16-
use OCP\IConfig;
1716
use OCP\IRequest;
1817
use Sabre\DAV\Exception\NotFound;
1918
use Sabre\DAV\INode;
@@ -32,12 +31,10 @@ class Plugin extends ServerPlugin {
3231
*
3332
* @param Auth $auth
3433
* @param IRequest $request
35-
* @param IConfig $config
3634
*/
3735
public function __construct(
3836
private Auth $auth,
3937
private IRequest $request,
40-
private IConfig $config,
4138
) {
4239
}
4340

@@ -147,13 +144,7 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
147144
// If there's no ACL support, we allow everything
148145
if ($acl) {
149146
/** @var \Sabre\DAVACL\Plugin $acl */
150-
$acl->checkPrivileges($path, '{DAV:}write');
151-
152-
$limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
153-
$isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
154-
if ($limitSharingToOwner && !$isOwner) {
155-
return;
156-
}
147+
$acl->checkPrivileges($path, '{DAV:}write-acl');
157148
}
158149

159150
$node->updateShares($message->set, $message->remove);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\DAV\Sharing;
11+
12+
trait SharingPrivilegeSetTrait {
13+
#[\Override]
14+
public function getSupportedPrivilegeSet(): array {
15+
return [
16+
'{DAV:}read' => [
17+
'abstract' => false,
18+
'aggregates' => [
19+
'{DAV:}read-acl' => ['abstract' => false, 'aggregates' => []],
20+
'{DAV:}read-current-user-privilege-set' => ['abstract' => false, 'aggregates' => []],
21+
],
22+
],
23+
'{DAV:}write' => [
24+
'abstract' => false,
25+
'aggregates' => [
26+
'{DAV:}write-properties' => ['abstract' => false, 'aggregates' => []],
27+
'{DAV:}write-content' => ['abstract' => false, 'aggregates' => []],
28+
'{DAV:}unlock' => ['abstract' => false, 'aggregates' => []],
29+
'{DAV:}bind' => ['abstract' => false, 'aggregates' => []],
30+
'{DAV:}unbind' => ['abstract' => false, 'aggregates' => []],
31+
],
32+
],
33+
'{DAV:}write-acl' => ['abstract' => false, 'aggregates' => []],
34+
];
35+
}
36+
}

‎apps/dav/lib/Server.php‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public function __construct(
179179

180180
// calendar plugins
181181
if ($this->requestIsForSubtree(['calendars', 'public-calendars', 'system-calendars', 'principals'])) {
182-
$this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig()));
182+
$this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
183183
$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
184184
$this->server->addPlugin(new ICSExportPlugin(\OC::$server->getConfig(), $logger));
185185
$this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), \OC::$server->get(DefaultCalendarValidator::class)));
@@ -192,7 +192,6 @@ public function __construct(
192192

193193
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
194194
$this->server->addPlugin(new PublishPlugin(
195-
\OC::$server->getConfig(),
196195
\OC::$server->getURLGenerator()
197196
));
198197

@@ -202,7 +201,7 @@ public function __construct(
202201

203202
// addressbook plugins
204203
if ($this->requestIsForSubtree(['addressbooks', 'principals'])) {
205-
$this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig()));
204+
$this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
206205
$this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin());
207206
$this->server->addPlugin(new VCFExportPlugin());
208207
$this->server->addPlugin(new MultiGetExportPlugin());

‎apps/dav/tests/unit/CalDAV/CalendarTest.php‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
use OCA\DAV\CalDAV\BirthdayService;
1010
use OCA\DAV\CalDAV\CalDavBackend;
1111
use OCA\DAV\CalDAV\Calendar;
12+
use OCA\DAV\Connector\Sabre\DavAclPlugin;
1213
use OCP\IConfig;
1314
use OCP\IL10N;
1415
use PHPUnit\Framework\MockObject\MockObject;
1516
use Psr\Log\LoggerInterface;
1617
use Sabre\DAV\PropPatch;
18+
use Sabre\DAV\Server;
19+
use Sabre\DAV\SimpleCollection;
1720
use Sabre\VObject\Reader;
1821
use Test\TestCase;
1922

@@ -231,6 +234,11 @@ public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet, $uri = 'def
231234
'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write',
232235
'protected' => true
233236
];
237+
$expectedAcl[] = [
238+
'privilege' => '{DAV:}write-acl',
239+
'principal' => $hasOwnerSet ? 'user1' : 'user2',
240+
'protected' => true
241+
];
234242
}
235243

236244
$expectedAcl[] = [
@@ -275,6 +283,59 @@ public function providesReadOnlyInfo() {
275283
];
276284
}
277285

286+
/**
287+
* @dataProvider providesSharingPrivileges
288+
*/
289+
public function testOnlyOwnerHoldsWriteAcl(string $principal, bool $readOnly, bool $expectsWrite, bool $expectsWriteAcl): void {
290+
/** @var CalDavBackend&MockObject $backend */
291+
$backend = $this->createMock(CalDavBackend::class);
292+
$backend->method('applyShareAcl')->willReturnArgument(1);
293+
$calendarInfo = [
294+
'{DAV:}displayname' => 'Test',
295+
'{http://owncloud.org/ns}owner-principal' => 'user1',
296+
'{http://owncloud.org/ns}read-only' => $readOnly,
297+
'principaluri' => 'user2',
298+
'id' => 666,
299+
'uri' => 'cal',
300+
];
301+
$calendar = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
302+
303+
$aclPlugin = new class($principal) extends DavAclPlugin {
304+
public function __construct(
305+
private string $principal,
306+
) {
307+
parent::__construct();
308+
}
309+
310+
#[\Override]
311+
public function getCurrentUserPrincipal() {
312+
return $this->principal;
313+
}
314+
315+
#[\Override]
316+
public function getPrincipalMembership($mainPrincipal) {
317+
return [];
318+
}
319+
};
320+
$server = new Server(new SimpleCollection('root'));
321+
$server->addPlugin($aclPlugin);
322+
323+
$privileges = $aclPlugin->getCurrentUserPrivilegeSet($calendar);
324+
325+
$this->assertSame($expectsWrite, in_array('{DAV:}write', $privileges, true));
326+
$this->assertSame($expectsWriteAcl, in_array('{DAV:}write-acl', $privileges, true));
327+
}
328+
329+
public static function providesSharingPrivileges(): array {
330+
return [
331+
'owner' => ['user1', false, true, true],
332+
'owner of read-only share' => ['user1', true, true, true],
333+
'owner write proxy' => ['user1/calendar-proxy-write', false, true, false],
334+
'read-write sharee' => ['user2', false, true, false],
335+
'read-only sharee' => ['user2', true, false, false],
336+
];
337+
}
338+
278339
/**
279340
* @dataProvider providesConfidentialClassificationData
280341
* @param int $expectedChildren

0 commit comments

Comments
 (0)