From 0fd1601a9adf6a13bfb39be7dcde8a06abbbf917 Mon Sep 17 00:00:00 2001 From: JpMaxMan Date: Thu, 7 May 2026 12:17:42 -0500 Subject: [PATCH] fix(authz): block non-admin members from changing duration on published events SummitEvent::setDuration delegates to _setDuration which, when start_date is set, silently shifts end_date to keep start_date + duration aligned. For an event that has already been published to the schedule, this means any non-admin caller (e.g., a track chair hitting PUT /events/{id} with {duration} via AbstractPublishService::updateDuration) could move a live slot with no audit, no notification, and no validation that the move is intended. Add the invariant at the entity setter so all callers are bound by the same rule, regardless of which controller or service initiated the call. The check intentionally skips when no member is supplied so trusted internal callers (e.g., SummitService::advanceSummit during bulk schedule shifts) continue to work. Pairs with the track-chairs UI gate in fntechgit/track-chairs#67. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Foundation/Summit/Events/SummitEvent.php | 6 +++++ tests/SummitEventModelTest.php | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/Models/Foundation/Summit/Events/SummitEvent.php b/app/Models/Foundation/Summit/Events/SummitEvent.php index 64ca573f2..1e4ad7979 100644 --- a/app/Models/Foundation/Summit/Events/SummitEvent.php +++ b/app/Models/Foundation/Summit/Events/SummitEvent.php @@ -1770,6 +1770,12 @@ public function setDuration(int $duration_in_seconds, bool $skipDatesSetting = f if (!$this->type->isAllowsPublishingDates()) { throw new ValidationException("Type does not allows Publishing Period."); } + // Once an event is published, only summit admins may change its duration. + // The published path silently shifts end_date via _setDuration when start_date + // is set, which would move a live schedule slot for any non-admin caller. + if ($this->isPublished() && !is_null($member) && !$member->isSummitAllowed($this->getSummit())) { + throw new ValidationException("Cannot modify duration of a published event."); + } $this->_setDuration($this->getSummit(), $duration_in_seconds, $skipDatesSetting, $member); } diff --git a/tests/SummitEventModelTest.php b/tests/SummitEventModelTest.php index a337efdbe..5ca6817f1 100644 --- a/tests/SummitEventModelTest.php +++ b/tests/SummitEventModelTest.php @@ -11,11 +11,13 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +use models\main\Member; use models\summit\SummitEvent; use models\summit\Presentation; use LaravelDoctrine\ORM\Facades\EntityManager; use Doctrine\Persistence\ObjectRepository; use Illuminate\Support\Facades\DB; +use models\exceptions\ValidationException; use DateTimeZone; use DateTime; use DateInterval; @@ -77,4 +79,28 @@ public function testChangingEndDateShouldRecalculateDuration(){ $new_duration = $presentation->getDuration(); $this->assertTrue($old_duration < $new_duration); } + + public function testNonAdminMemberCannotChangeDurationOnPublishedEvent(){ + $presentation = self::$presentations[0]; + $this->assertTrue($presentation->isPublished()); + + $member = Mockery::mock(Member::class)->makePartial(); + $member->shouldReceive('isSummitAllowed')->andReturn(false); + + $this->expectException(ValidationException::class); + $presentation->setDuration(864000, false, $member); + } + + public function testAdminMemberCanChangeDurationOnPublishedEvent(){ + $presentation = self::$presentations[0]; + $this->assertTrue($presentation->isPublished()); + + $member = Mockery::mock(Member::class)->makePartial(); + $member->shouldReceive('isSummitAllowed')->andReturn(true); + + $old_end_date = $presentation->getEndDate(); + $presentation->setDuration(864000, false, $member); + $new_end_date = $presentation->getEndDate(); + $this->assertTrue($old_end_date < $new_end_date); + } } \ No newline at end of file