From 75c52ad2c27e7d7c4f3202db30712fda6c484e3b Mon Sep 17 00:00:00 2001 From: Anne-Cath Date: Mon, 25 Aug 2025 17:52:10 +0200 Subject: [PATCH] Returns object if the current user is allowed to do the $sAction on an $sObjectClass object (with $sObjectId id) --- .../portal/src/Helper/SecurityHelper.php | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/datamodels/2.x/itop-portal-base/portal/src/Helper/SecurityHelper.php b/datamodels/2.x/itop-portal-base/portal/src/Helper/SecurityHelper.php index 1b95179d03..e77f6aaf2b 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Helper/SecurityHelper.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Helper/SecurityHelper.php @@ -20,6 +20,7 @@ namespace Combodo\iTop\Portal\Helper; +use CoreException; use LogChannels; use UserRights; use IssueLog; @@ -174,6 +175,123 @@ public function IsActionAllowed($sAction, $sObjectClass, $sObjectId = null) return true; } + + /** + * Returns object if the current user is allowed to do the $sAction on an $sObjectClass object (with $sObjectId id) + * Checks are: + * - Has a scope query for the $sObjectClass /$sObjectId/ $sAction + * - Is allowed by datamodel for $sObjectClass / $sAction + * + * @param string $sAction Must be in UR_ACTION_READ|UR_ACTION_MODIFY|UR_ACTION_CREATE + * @param string $sObjectClass + * @param string $sObjectId + * @param bool $bMustBeFound + * + * @throws \CoreExceptionif no result found and $bMustBeFound=true + * @throws \MissingQueryArgument + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \OQLException + */ + public function GetObjectForAction($sAction, $sObjectClass, $sObjectId, $bMustBeFound) + { + $sDebugTracePrefix = __CLASS__.' / '.__METHOD__.' : Returned object for action '.$sAction.' on '.$sObjectClass.'::'.$sObjectId; + + // Forcing allowed writing on the object if necessary. This is used in some particular cases. + $bObjectIsCurrentUser = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId()); + if(in_array($sAction , array(UR_ACTION_MODIFY, UR_ACTION_READ)) && $bObjectIsCurrentUser){ + return MetaModel::GetObject($sObjectClass, $sObjectId, true,true); + } + + // Checking the scopes layer + // - Transforming scope action as there is only 2 values + $sScopeAction = ($sAction === UR_ACTION_READ) ? UR_ACTION_READ : UR_ACTION_MODIFY; + // - Retrieving the query. If user has no scope, it can't access that kind of objects + $oScopeQuery = $this->oScopeValidator->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sObjectClass, $sScopeAction); + if ($oScopeQuery === null) + { + IssueLog::Debug($sDebugTracePrefix.' as there was no scope defined for action '.$sScopeAction.' and profiles '.implode('/', UserRights::ListProfiles()), LogChannels::PORTAL); + if ($bMustBeFound) + { + $sNotFoundErrorMessage = "No result for the single row query"; + IssueLog::Info($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [ + 'class' => $sObjectClass, + 'key' => $sObjectId + ]); + throw new CoreException($sNotFoundErrorMessage); + } + return null; + } + + // Checking if object status is in cache (to avoid unnecessary query) + if (isset(static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId])) { + if (static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId] === false) + { + IssueLog::Debug($sDebugTracePrefix.' as it was denied in the scope objects cache', LogChannels::PORTAL); + if ($bMustBeFound) + { + $sNotFoundErrorMessage = "No result for the single row query"; + IssueLog::Info($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [ + 'class' => $sObjectClass, + 'key' => $sObjectId + ]); + throw new CoreException($sNotFoundErrorMessage); + } + return null; + } else { + return MetaModel::GetObject($sObjectClass, $sObjectId, true,true); + } + } + else + { + + // Modifying query to filter on the ID + // - Adding expression + $sObjectKeyAtt = MetaModel::DBGetKey($sObjectClass); + $oFieldExp = new FieldExpression($sObjectKeyAtt, $oScopeQuery->GetClassAlias()); + $oBinExp = new BinaryExpression($oFieldExp, '=', new VariableExpression('object_id')); + $oScopeQuery->AddConditionExpression($oBinExp); + // - Setting value + $aQueryParams = $oScopeQuery->GetInternalParams(); + $aQueryParams['object_id'] = $sObjectId; + $oScopeQuery->SetInternalParams($aQueryParams); + unset($aQueryParams); + IssueLog::Error('requete:'.$oScopeQuery->ToOQL(true)); + // - Checking if query result is null (which means that the user has no right to view this specific object) + $oSet = new DBObjectSet($oScopeQuery); + $oObject = $oSet->Fetch(); + if ($oObject === null) + { + // Updating cache + static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId] = false; + IssueLog::Debug($sDebugTracePrefix.' as there was no result for the following scope query : '.$oScopeQuery->ToOQL(true), LogChannels::PORTAL); + } else { + // Updating cache + static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId] = true; + } + } + + // Checking reading security layer. The object could be listed, check if it is actually allowed to view it + if (UserRights::IsActionAllowed($sObjectClass, $sAction) == UR_ALLOWED_NO) { + // For security reasons, we don't want to give the user too many information on why he cannot access the object. + //throw new SecurityException('User not allowed to view this object', array('class' => $sObjectClass, 'id' => $sObjectId)); + IssueLog::Debug($sDebugTracePrefix.' as the user is not allowed to access this object according to the datamodel security (cf. Console settings)', LogChannels::PORTAL); + $oObject = null; + } + + if ($bMustBeFound && $oObject === null ) + { + $sNotFoundErrorMessage = "No result for the single row query"; + IssueLog::Info($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [ + 'class' => $sObjectClass, + 'key' => $sObjectId + ]); + throw new CoreException($sNotFoundErrorMessage); + } + + return $oObject; + } + /** * @param string $sStimulusCode * @param string $sObjectClass