diff --git a/lib/CalDAV/OrganizerValidationPlugin.php b/lib/CalDAV/OrganizerValidationPlugin.php index d768a49b..8fa2b2b7 100644 --- a/lib/CalDAV/OrganizerValidationPlugin.php +++ b/lib/CalDAV/OrganizerValidationPlugin.php @@ -22,6 +22,7 @@ function initialize(Server $server) { $this->server = $server; $server->on('calendarObjectChange', [$this, 'calendarObjectChange'], Plugin::PRIORITY_BEFORE_SCHEDULING - 10); $server->on('beforeMove', [$this, 'beforeMove'], 45); + $server->on('beforeCopy', [$this, 'beforeCopy'], 45); } function getPluginName() { @@ -55,8 +56,15 @@ function calendarObjectChange( } function beforeMove($sourcePath, $destinationPath) { + // A team calendar lives in its own calendar home, so a legitimate MOVE into one always + // crosses homes. Exempt exactly that destination from the guard, because it is the one + // the ORGANIZER validation below covers instead. list($calendarPath,) = Utils::splitEventPath('/' . ltrim($destinationPath, '/')); - if (!$calendarPath || !$this->isTeamCalendarPath($calendarPath)) return; + if (!$calendarPath || !$this->isTeamCalendarPath($calendarPath)) { + $this->assertSameCalendarHome($sourcePath, $destinationPath); + + return; + } try { $source = $this->server->tree->getNodeForPath($sourcePath); @@ -80,6 +88,38 @@ function beforeMove($sourcePath, $destinationPath) { } } + function beforeCopy($sourcePath, $destinationPath, $depth = null) { + $this->assertSameCalendarHome($sourcePath, $destinationPath); + } + + /** + * SabreDAV runs COPY and MOVE through Tree::copy()/Tree::move(), which never emit + * calendarObjectChange and therefore skip the ORGANIZER validation a PUT goes through. + * Confining a transfer to a single calendar home keeps an event that a PUT would have + * rejected from being smuggled into someone else's calendar. + */ + private function assertSameCalendarHome($sourcePath, $destinationPath): void { + $sourceHome = $this->calendarHomeOf($sourcePath); + $destinationHome = $this->calendarHomeOf($destinationPath); + + // Not a calendar object on either end: collection level transfers are left to the ACL plugin. + if ($sourceHome === null || $destinationHome === null) return; + + if ($sourceHome !== $destinationHome) { + throw new Forbidden('COPY and MOVE are only allowed within the same calendar home.'); + } + } + + private function calendarHomeOf($eventPath): ?string { + list($calendarPath,) = Utils::splitEventPath('/' . ltrim($eventPath, '/')); + if (!$calendarPath) { + return null; + } + + // $calendarPath is 'calendars/{baseId}/{calendarUri}'. + return explode('/', $calendarPath)[1]; + } + private function validateCalendarOrganizer(VCalendar $calendar, $calendarPath, ?string $existingOrganizerUri = null): void { $vevents = $calendar->select('VEVENT'); if (empty($vevents) || !($organizerUri = $this->extractOrganizerUri($vevents))) return; diff --git a/run_test.sh b/run_test.sh index 1c3879a5..6da96fd5 100755 --- a/run_test.sh +++ b/run_test.sh @@ -75,6 +75,7 @@ trap cleanup EXIT # finally: sera exécuté *quoi qu'il arrive* javatest() { git clone https://github.com/linagora/twake-calendar-integration-tests.git it-tests || exit 1 cd it-tests || exit 1 + git checkout test-sabre-450 || exit 1 bash pre-build.sh esn_sabre_test || exit 1 mvn clean install -Dapi.version=1.43 -Dtest=com.linagora.dav.sabrev4_7.** -Damqp.scheduling.enabled=true || exit 1 }