securityhelper.class.inc.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php
  2. // Copyright (C) 2010-2017 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. namespace Combodo\iTop\Portal\Helper;
  19. use \Exception;
  20. use \Silex\Application;
  21. use \utils;
  22. use \UserRights;
  23. use \Dict;
  24. use \IssueLog;
  25. use \MetaModel;
  26. use \DBSearch;
  27. use \DBObjectSearch;
  28. use \DBObjectSet;
  29. use \FieldExpression;
  30. use \VariableExpression;
  31. use \BinaryExpression;
  32. use \Combodo\iTop\Portal\Helper\ScopeValidatorHelper;
  33. /**
  34. * SecurityHelper class
  35. *
  36. * Handle security checks through the different layers (portal scopes, iTop silos, user rights)
  37. *
  38. * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
  39. */
  40. class SecurityHelper
  41. {
  42. public static $aAllowedScopeObjectsCache = array(
  43. UR_ACTION_READ => array(),
  44. UR_ACTION_MODIFY => array(),
  45. );
  46. /**
  47. * Returns true if the current user is allowed to do the $sAction on an $sObjectClass object (with optionnal $sObjectId id)
  48. * Checks are:
  49. * - Has a scope query for the $sObjectClass / $sAction
  50. * - Optionally, if $sObjectId provided: Is object within scope for $sObjectClass / $sObjectId / $sAction
  51. * - Is allowed by datamodel for $sObjectClass / $sAction
  52. *
  53. * @param Silex\Application $oApp
  54. * @param string $sAction Must be in UR_ACTION_READ|UR_ACTION_MODIFY|UR_ACTION_CREATE
  55. * @param string $sObjectClass
  56. * @param string $sObjectId
  57. * @return boolean
  58. */
  59. public static function IsActionAllowed(Application $oApp, $sAction, $sObjectClass, $sObjectId = null)
  60. {
  61. $sDebugTracePrefix = __CLASS__ . ' / ' . __METHOD__ . ' : Returned false for action ' . $sAction . ' on ' . $sObjectClass . '::' . $sObjectId;
  62. // Checking action type
  63. if (!in_array($sAction, array(UR_ACTION_READ, UR_ACTION_MODIFY, UR_ACTION_CREATE)))
  64. {
  65. if ($oApp['debug'])
  66. {
  67. IssueLog::Info($sDebugTracePrefix . ' as the action value could not be understood (' . UR_ACTION_READ . '/' . UR_ACTION_MODIFY . '/' . UR_ACTION_CREATE . ' expected');
  68. }
  69. return false;
  70. }
  71. // Checking the scopes layer
  72. // - Transforming scope action as there is only 2 values
  73. $sScopeAction = ($sAction === UR_ACTION_READ) ? UR_ACTION_READ : UR_ACTION_MODIFY;
  74. // - Retrieving the query. If user has no scope, it can't access that kind of objects
  75. $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sObjectClass, $sScopeAction);
  76. if ($oScopeQuery === null)
  77. {
  78. if ($oApp['debug'])
  79. {
  80. IssueLog::Info($sDebugTracePrefix . ' as there was no scope defined for action ' . $sScopeAction . ' and profiles ' . implode('/', UserRights::ListProfiles()));
  81. }
  82. return false;
  83. }
  84. // - If action != create we do some additionnal checks
  85. if ($sAction !== UR_ACTION_CREATE)
  86. {
  87. // - Checking specific object if id is specified
  88. if ($sObjectId !== null)
  89. {
  90. // Checking if object status is in cache (to avoid unnecessary query)
  91. if(isset(static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId]) )
  92. {
  93. if(static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId] === false)
  94. {
  95. if ($oApp['debug'])
  96. {
  97. IssueLog::Info($sDebugTracePrefix . ' as it was denied in the scope objects cache');
  98. }
  99. return false;
  100. }
  101. }
  102. else
  103. {
  104. // Modifying query to filter on the ID
  105. // - Adding expression
  106. $sObjectKeyAtt = MetaModel::DBGetKey($sObjectClass);
  107. $oFieldExp = new FieldExpression($sObjectKeyAtt, $oScopeQuery->GetClassAlias());
  108. $oBinExp = new BinaryExpression($oFieldExp, '=', new VariableExpression('object_id'));
  109. $oScopeQuery->AddConditionExpression($oBinExp);
  110. // - Setting value
  111. $aQueryParams = $oScopeQuery->GetInternalParams();
  112. $aQueryParams['object_id'] = $sObjectId;
  113. $oScopeQuery->SetInternalParams($aQueryParams);
  114. unset($aQueryParams);
  115. // - Checking if query result is null (which means that the user has no right to view this specific object)
  116. $oSet = new DBObjectSet($oScopeQuery);
  117. if ($oSet->Count() === 0)
  118. {
  119. // Updating cache
  120. static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId] = false;
  121. if ($oApp['debug'])
  122. {
  123. IssueLog::Info($sDebugTracePrefix . ' as there was no result for the following scope query : ' . $oScopeQuery->ToOQL(true));
  124. }
  125. return false;
  126. }
  127. // Updating cache
  128. static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass][$sObjectId] = true;
  129. }
  130. }
  131. }
  132. // Checking reading security layer. The object could be listed, check if it is actually allowed to view it
  133. if (UserRights::IsActionAllowed($sObjectClass, $sAction) == UR_ALLOWED_NO)
  134. {
  135. // For security reasons, we don't want to give the user too many informations on why he cannot access the object.
  136. //throw new SecurityException('User not allowed to view this object', array('class' => $sObjectClass, 'id' => $sObjectId));
  137. if ($oApp['debug'])
  138. {
  139. IssueLog::Info($sDebugTracePrefix . ' as the user is not allowed to access this object according to the datamodel security (cf. Console settings)');
  140. }
  141. return false;
  142. }
  143. return true;
  144. }
  145. public static function IsStimulusAllowed(Application $oApp, $sStimulusCode, $sObjectClass, $oInstanceSet = null)
  146. {
  147. // Checking DataModel layer
  148. $aStimuliFromDatamodel = Metamodel::EnumStimuli($sObjectClass);
  149. $iActionAllowed = (get_class($aStimuliFromDatamodel[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sObjectClass, $sStimulusCode, $oInstanceSet) : UR_ALLOWED_NO;
  150. if( ($iActionAllowed === false) || ($iActionAllowed === UR_ALLOWED_NO) )
  151. {
  152. return false;
  153. }
  154. // Checking portal security layer
  155. $aStimuliFromPortal = $oApp['lifecycle_validator']->GetStimuliForProfiles(UserRights::ListProfiles(), $sObjectClass);
  156. if(!in_array($sStimulusCode, $aStimuliFromPortal))
  157. {
  158. return false;
  159. }
  160. return true;
  161. }
  162. /**
  163. * Preloads scope objects cache with objects from $oQuery
  164. *
  165. * @param Application $oApp
  166. * @param DBSearch $oSet
  167. * @param array $aExtKeysToPreload
  168. */
  169. public static function PreloadForCache(Application $oApp, DBSearch $oSearch, $aExtKeysToPreload = null)
  170. {
  171. $sObjectClass = $oSearch->GetClass();
  172. $aObjectIds = array();
  173. $aExtKeysIds = array();
  174. $aColumnsToLoad = array();
  175. if($aExtKeysToPreload !== null)
  176. {
  177. foreach($aExtKeysToPreload as $sAttCode)
  178. {
  179. /** @var \AttributeDefinition $oAttDef */
  180. $oAttDef = MetaModel::GetAttributeDef($sObjectClass, $sAttCode);
  181. if($oAttDef->IsExternalKey())
  182. {
  183. $aExtKeysIds[$oAttDef->GetTargetClass()] = array();
  184. $aColumnsToLoad[] = $sAttCode;
  185. }
  186. }
  187. }
  188. // Retrieving IDs of all objects
  189. // Note: We have to clone $oSet otherwise the source object will be modified
  190. $oSet = new DBObjectSet($oSearch);
  191. $oSet->OptimizeColumnLoad(array($oSearch->GetClassAlias() => $aColumnsToLoad));
  192. while($oCurrentRow = $oSet->Fetch())
  193. {
  194. // Note: By presetting value to false, it is quicker to find which objects where not returned by the scope query later
  195. $aObjectIds[$oCurrentRow->GetKey()] = false;
  196. // Preparing ExtKeys to preload
  197. foreach($aColumnsToLoad as $sAttCode)
  198. {
  199. $iExtKey = $oCurrentRow->Get($sAttCode);
  200. if($iExtKey > 0)
  201. {
  202. /** @var \AttributeExternalKey $oAttDef */
  203. $oAttDef = MetaModel::GetAttributeDef($sObjectClass, $sAttCode);
  204. if(!in_array($iExtKey, $aExtKeysIds[$oAttDef->GetTargetClass()]))
  205. {
  206. $aExtKeysIds[$oAttDef->GetTargetClass()][] = $iExtKey;
  207. }
  208. }
  209. }
  210. }
  211. foreach(array(UR_ACTION_READ, UR_ACTION_MODIFY) as $sScopeAction)
  212. {
  213. // Retrieving scope query
  214. /** @var DBSearch $oScopeQuery */
  215. $oScopeQuery = $oApp['scope_validator']->GetScopeFilterForProfiles(UserRights::ListProfiles(), $sObjectClass, $sScopeAction);
  216. if($oScopeQuery !== null)
  217. {
  218. // Restricting scope if specified
  219. if(!empty($aObjectIds))
  220. {
  221. $oScopeQuery->AddCondition('id', array_keys($aObjectIds), 'IN');
  222. }
  223. // Preparing object set
  224. $oScopeSet = new DBObjectSet($oScopeQuery);
  225. $oScopeSet->OptimizeColumnLoad(array());
  226. // Checking objects status
  227. $aScopeObjectIds = $aObjectIds;
  228. while($oCurrentRow = $oScopeSet->Fetch())
  229. {
  230. $aScopeObjectIds[$oCurrentRow->GetKey()] = true;
  231. }
  232. // Updating cache
  233. if(!isset(static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass]))
  234. {
  235. static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass] = $aScopeObjectIds;
  236. }
  237. else
  238. {
  239. static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass] = array_merge_recursive(static::$aAllowedScopeObjectsCache[$sScopeAction][$sObjectClass], $aScopeObjectIds);
  240. }
  241. }
  242. }
  243. // Preloading ExtKeys
  244. foreach($aExtKeysIds as $sTargetClass => $aTargetIds)
  245. {
  246. if(!empty($aTargetIds))
  247. {
  248. $oTargetSearch = new DBObjectSearch($sTargetClass);
  249. $oTargetSearch->AddCondition('id', $aTargetIds, 'IN');
  250. static::PreloadForCache($oApp, $oTargetSearch);
  251. }
  252. }
  253. }
  254. }