diff --git a/core/ormdocument.class.inc.php b/core/ormdocument.class.inc.php index 7e7b2bb20b..0f9b4312ba 100644 --- a/core/ormdocument.class.inc.php +++ b/core/ormdocument.class.inc.php @@ -340,13 +340,14 @@ class ormDocument * @param string $sContentDisposition Either 'inline' or 'attachment' * @param string $sSecretField The attcode of the field containing a "secret" to be provided in order to retrieve the file * @param string $sSecretValue The value of the secret to be compared with the value of the attribute $sSecretField + * @param bool $bAllowAllData If true, no rights filtering is applied * * @return void */ - public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null) + public static function DownloadDocument(WebPage $oPage, $sClass, $id, $sAttCode, $sContentDisposition = 'attachment', $sSecretField = null, $sSecretValue = null, $bAllowAllData = false) { try { - $oObj = MetaModel::GetObject($sClass, $id, false, false); + $oObj = MetaModel::GetObject($sClass, $id, false, $bAllowAllData); if (!is_object($oObj)) { // If access to the document is not granted, check if the access to the host object is allowed $oObj = MetaModel::GetObject($sClass, $id, false, true); diff --git a/sources/Controller/Newsroom/iTopNewsroomController.php b/sources/Controller/Newsroom/iTopNewsroomController.php index 37ea7bebfd..d27da85c58 100644 --- a/sources/Controller/Newsroom/iTopNewsroomController.php +++ b/sources/Controller/Newsroom/iTopNewsroomController.php @@ -16,10 +16,12 @@ use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory; use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory; use Combodo\iTop\Application\UI\Base\Layout\Object\ObjectSummary; use Combodo\iTop\Application\UI\Base\Layout\UIContentBlock; +use Combodo\iTop\Application\WebPage\DownloadPage; use Combodo\iTop\Application\WebPage\iTopWebPage; use Combodo\iTop\Application\WebPage\JsonPage; use Combodo\iTop\Application\WebPage\JsonPPage; use Combodo\iTop\Controller\Notifications\NotificationsCenterController; +use Combodo\iTop\Service\Notification\Event\EventNotificationNewsroomService; use Combodo\iTop\Service\Notification\NotificationsRepository; use Combodo\iTop\Service\Router\Router; use CoreException; @@ -27,6 +29,7 @@ use DBObjectSearch; use DBObjectSet; use Dict; use EventNotificationNewsroom; +use Exception; use MetaModel; use SecurityException; use UserRights; @@ -376,9 +379,10 @@ JS $oEventBlock->SetCSSColorClass($sReadColor); $oEventBlock->SetSubTitle($sReadLabel); $oEventBlock->SetClassLabel(''); + /** @var \ormDocument $oImage */ $oImage = $oEvent->Get('icon'); if (!$oImage->IsEmpty()) { - $sIconUrl = $oImage->GetDisplayURL(get_class($oEvent), $iEventId, 'icon'); + $sIconUrl = self::GetDisplayIconUrl($iEventId, $oImage->GetSignature()); $oEventBlock->SetIcon($sIconUrl, Panel::ENUM_ICON_COVER_METHOD_COVER, true); } @@ -542,7 +546,7 @@ $sMessage HTML; $sIcon = $oMessage->Get('icon') !== null ? - $oMessage->Get('icon')->GetDisplayURL(EventNotificationNewsroom::class, $oMessage->GetKey(), 'icon') : + $this->GetDisplayIconUrl($oMessage->GetKey(), $oMessage->Get('icon')->GetSignature()) : Branding::GetCompactMainLogoAbsoluteUrl(); $aMessages[] = [ 'id' => $oMessage->GetKey(), @@ -689,6 +693,35 @@ HTML; return $oPage; } + /** + * Display the icon of an EventNotificationNewsroom + * (copy of ajax.render.php?operation=display_document but with the bAllowAllData parameter set to true in order to bypass the data access restrictions since the icon is not a critical information) + * @return void + * @throws \ConfigException + * @throws \CoreException + */ + public function OperationViewIcon(): void + { + $sId = utils::ReadParam('id', ''); + if (!empty($sId)) { + $oPage = new DownloadPage(''); + // X-Frame http header : set in page constructor, but we need to allow frame integration for this specific page + // so we're resetting its value ! (see N°3416) + $oPage->add_xframe_options(''); + $iCacheSec = (int)utils::ReadParam('cache', 0); + $oPage->set_cache($iCacheSec); + + // N°4129 - Prevent XSS attacks & other script executions + if (utils::GetConfig()->Get('security.disable_inline_documents_sandbox') === false) { + $oPage->add_header('Content-Security-Policy: sandbox;'); + } + + if (EventNotificationNewsroomService::DownloadIcon($oPage, $sId, UserRights::GetContactId()) === true) { + $oPage->Output(); + } + } + } + /** * @param string $sAction * @@ -781,4 +814,9 @@ HTML; return $aReturnData; } + + protected function GetDisplayIconUrl(string $sId, string $sSignature): string + { + return utils::GetAbsoluteUrlAppRoot()."pages/UI.php?route=itopnewsroom.view_icon&id=$sId&s=$sSignature&cache=86400"; + } } diff --git a/sources/Service/Notification/Event/EventNotificationNewsroomService.php b/sources/Service/Notification/Event/EventNotificationNewsroomService.php index 79de5a84e2..8cc4db3214 100644 --- a/sources/Service/Notification/Event/EventNotificationNewsroomService.php +++ b/sources/Service/Notification/Event/EventNotificationNewsroomService.php @@ -4,8 +4,10 @@ namespace Combodo\iTop\Service\Notification\Event; use Action; use Combodo\iTop\Application\Branding; +use Combodo\iTop\Application\WebPage\WebPage; use EventNotificationNewsroom; use MetaModel; +use ormDocument; use utils; /** @@ -70,4 +72,31 @@ class EventNotificationNewsroomService return $oEvent; } + + /** + * @param \Combodo\iTop\Application\WebPage\WebPage $oPage + * @param string $sId + * @param int $iContactId + * + * @return bool Returns true if the download has been launched, false otherwise (e.g. if the event doesn't exist or doesn't belong to the current user) + * @throws \ArchivedObjectException + * @throws \CoreException + */ + public static function DownloadIcon(WebPage $oPage, string $sId, int $iContactId): bool + { + $oEvent = MetaModel::GetObject(EventNotificationNewsroom::class, $sId, false, true); + if (($oEvent !== null) && ($oEvent->Get('contact_id') === $iContactId)) { + ormDocument::DownloadDocument( + $oPage, + EventNotificationNewsroom::class, + $sId, + 'icon', + ormDocument::ENUM_CONTENT_DISPOSITION_INLINE, + bAllowAllData: true + ); + return true; + } + + return false; + } } diff --git a/tests/php-unit-tests/unitary-tests/core/ormDocumentTest.php b/tests/php-unit-tests/unitary-tests/core/ormDocumentTest.php index b9e7a090c4..6303e3a5d8 100644 --- a/tests/php-unit-tests/unitary-tests/core/ormDocumentTest.php +++ b/tests/php-unit-tests/unitary-tests/core/ormDocumentTest.php @@ -198,6 +198,21 @@ class ormDocumentTest extends ItopDataTestCase $this->assertStringNotContainsString('the object does not exist or you are not allowed to view it', $sAllowedHtml, 'Unexpected error message when rights are sufficient.'); } + /** + * @dataProvider DownloadDocumentRightsProvider + */ + public function testAllowsDownloadingDocumentWhenBypassingRightsChecksWithAllowAllData(string $sTargetClass, string $sAttCode, string $sData, string $sFileName, ?string $sHostClass) + { + $iDeniedDocumentId = $this->CreateDownloadTargetInOrg($sTargetClass, $sAttCode, $this->iOrgDifferentFromUser, $sData, $sFileName, $sHostClass); + + $oPageAllowed = new CaptureWebPage(); + ormDocument::DownloadDocument($oPageAllowed, $sTargetClass, $iDeniedDocumentId, $sAttCode, ormDocument::ENUM_CONTENT_DISPOSITION_INLINE, bAllowAllData: true); + $sAllowedHtml = $oPageAllowed->GetHtml(); + + $this->assertStringContainsString($sData, $sAllowedHtml, 'Expected file data present when bypassing rights checks.'); + $this->assertStringNotContainsString("Invalid id ($iDeniedDocumentId) for class '$sTargetClass' - the object does not exist or you are not allowed to view it", $sAllowedHtml, 'Unexpected invalid id error message when bypassing rights checks.'); + } + public function DownloadDocumentRightsProvider(): array { return [ diff --git a/tests/php-unit-tests/unitary-tests/sources/Service/Notification/Event/EventNotificationNewsroomServiceTest.php b/tests/php-unit-tests/unitary-tests/sources/Service/Notification/Event/EventNotificationNewsroomServiceTest.php new file mode 100644 index 0000000000..0130fc8571 --- /dev/null +++ b/tests/php-unit-tests/unitary-tests/sources/Service/Notification/Event/EventNotificationNewsroomServiceTest.php @@ -0,0 +1,154 @@ +createObject(Person::class, [ + 'name' => 'Khalo', + 'first_name' => 'Frida', + 'org_id' => $this->getTestOrgId(), + ]); + $this->oContact = $oContact; + + /** @var Trigger $oTrigger */ + $oTrigger = $this->createObject(TriggerOnObjectMention::class, [ + 'description' => 'Person mentioned on Ticket', + 'target_class' => 'Ticket', + ]); + $this->oTrigger = $oTrigger; + + /** @var Action $oAction */ + $oAction = $this->createObject(ActionNewsroom::class, [ + 'name' => 'Notification to persons mentioned in logs', + 'status' => 'enabled', + 'title' => '$this->friendlyname$', + 'message' => 'You have been mentioned by $current_contact->friendlyname$', + 'recipients' => 'SELECT Person WHERE id = :mentioned->id', + ]); + $this->oAction = $oAction; + + /** @var Ticket $oTicket */ + $oTicket = $this->createObject(UserRequest::class, [ + 'org_id' => $this->getTestOrgId(), + 'title' => 'Houston, got a problem', + 'description' => 'Test description', + ]); + $this->oTicket = $oTicket; + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->oEvent->DBDelete(); + } + + public function testDownloadIsTriggeredWhenDownloaderIsNotificationRecipient(): void + { + $this->oEvent = EventNotificationNewsroomService::MakeEventFromAction( + oAction: $this->oAction, + iContactId: $this->oContact->GetKey(), + iTriggerId: $this->oTrigger->GetKey(), + sMessage: 'Test message', + sTitle: 'Test event', + sUrl: 'https://localhost/itop/pages/UI.php?operation=details&class=UserRequest&id=1', + iObjectId: $this->oTicket->GetKey(), + sObjectClass: UserRequest::class, + ); + $this->oEvent->DBInsert(); + + $oPage = new CaptureWebPage(); + $bDownloadIcon = EventNotificationNewsroomService::DownloadIcon($oPage, $this->oEvent->GetKey(), $this->oContact->GetKey()); + $sHtml = $oPage->GetHtml(); + + $this->assertTrue($bDownloadIcon); + $this->assertNotEquals('', $sHtml); + } + + public function testDownloadIsNotTriggeredWhenDownloaderIsNotNotificationRecipient(): void + { + $oContact = $this->createObject(Person::class, [ + 'name' => 'Doe', + 'first_name' => 'John', + 'org_id' => $this->getTestOrgId(), + ]); + $this->oEvent = EventNotificationNewsroomService::MakeEventFromAction( + oAction: $this->oAction, + iContactId: $oContact->GetKey(), + iTriggerId: $this->oTrigger->GetKey(), + sMessage: 'Test message', + sTitle: 'Test event', + sUrl: 'https://localhost/itop/pages/UI.php?operation=details&class=UserRequest&id=1', + iObjectId: $this->oTicket->GetKey(), + sObjectClass: UserRequest::class, + ); + $this->oEvent->DBInsert(); + + $oPage = new CaptureWebPage(); + $bDownloadIcon = EventNotificationNewsroomService::DownloadIcon($oPage, $this->oEvent->GetKey(), $this->oContact->GetKey()); + $sHtml = $oPage->GetHtml(); + + $this->assertFalse($bDownloadIcon); + $this->assertEquals('', $sHtml); + } + + public function testDownloadIconIsTriggeredEvenWhenUserCannotReadIconAttribute(): void + { + // Create a user with Support Agent Profile + $sLogin = uniqid('EventNotificationNewsroomServiceTest'); + $oUser = $this->CreateContactlessUser($sLogin, self::$aURP_Profiles['Support Agent'], '1234@Abcdefg'); + $oUser->Set('contactid', $this->oContact->GetKey()); + UserRights::Login($sLogin); + + $this->oEvent = EventNotificationNewsroomService::MakeEventFromAction( + oAction: $this->oAction, + iContactId: $this->oContact->GetKey(), + iTriggerId: $this->oTrigger->GetKey(), + sMessage: 'Test message', + sTitle: 'Test event', + sUrl: 'https://localhost/itop/pages/UI.php?operation=details&class=UserRequest&id=1', + iObjectId: $this->oTicket->GetKey(), + sObjectClass: UserRequest::class, + ); + $this->oEvent->DBInsert(); + + $iURValue = UserRights::IsActionAllowedOnAttribute(EventNotificationNewsroom::class, 'icon', UR_ACTION_READ, $this->oEvent, $oUser); + $this->assertEquals(UR_ALLOWED_NO, $iURValue); + + $oPage = new CaptureWebPage(); + $bDownloadIcon = EventNotificationNewsroomService::DownloadIcon($oPage, $this->oEvent->GetKey(), $this->oContact->GetKey()); + $sHtml = $oPage->GetHtml(); + + $this->assertTrue($bDownloadIcon); + $this->assertNotEquals('', $sHtml); + $this->assertStringNotContainsString('the object does not exist or you are not allowed to view it', $sHtml); + } +}