diff --git a/addons/userrights/userrightsprofile.class.inc.php b/addons/userrights/userrightsprofile.class.inc.php index 9a99fd736..8889996ec 100644 --- a/addons/userrights/userrightsprofile.class.inc.php +++ b/addons/userrights/userrightsprofile.class.inc.php @@ -921,8 +921,9 @@ class UserRightsProfile extends UserRightsAddOnAPI } /** - * Find out which attribute is corresponding the the dimension 'owner org' - * returns null if no such attribute has been found (no filtering should occur) + * @param string $sClass + * @return string|null Find out which attribute is corresponding the dimension 'owner org' + * returns null if no such attribute has been found (no filtering should occur) */ public static function GetOwnerOrganizationAttCode($sClass) { diff --git a/addons/userrights/userrightsprofile.db.class.inc.php b/addons/userrights/userrightsprofile.db.class.inc.php index ac5b2277b..1eaac996d 100644 --- a/addons/userrights/userrightsprofile.db.class.inc.php +++ b/addons/userrights/userrightsprofile.db.class.inc.php @@ -586,10 +586,10 @@ class UserRightsProfile extends UserRightsAddOnAPI /** * Read and cache organizations allowed to the given user * - * @param $oUser - * @param $sClass (not used here but can be used in overloads) + * @param User $oUser + * @param string $sClass (not used here but can be used in overloads) * - * @return array + * @return array keys of the User allowed org * @throws \CoreException * @throws \Exception */ diff --git a/application/applicationextension.inc.php b/application/applicationextension.inc.php index 1b64d2668..f0cdbca53 100644 --- a/application/applicationextension.inc.php +++ b/application/applicationextension.inc.php @@ -1949,6 +1949,8 @@ class RestUtils * * @return DBObject The object found * @throws Exception If the input structure is not valid or it could not find exactly one object + * + * @see DBObject::CheckChangedExtKeysValues() generic method to check that we can access the linked object isn't used in that use case because values can be literal, OQL, friendlyname */ public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false) { diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 0a96af064..e08ec23db 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -5281,6 +5281,11 @@ EOF 'errors' => '

'.($bResult ? '' : implode('

', $aErrorsToDisplay)).'

', ); if ($bResult && (!$bPreview)) { + // doing the check will load multiple times same objects :/ + // but it shouldn't cost too much on execution time + // user can mitigate by selecting less extkeys/lnk to set and/or less objects to update 🤷‍♂️ + $oObj->CheckChangedExtKeysValues(); + $oObj->DBUpdate(); } } diff --git a/application/exceptions/InvalidExternalKeyValueException.php b/application/exceptions/InvalidExternalKeyValueException.php new file mode 100644 index 000000000..cd8a08990 --- /dev/null +++ b/application/exceptions/InvalidExternalKeyValueException.php @@ -0,0 +1,36 @@ +GetKey(); + $aContextData[self::ENUM_PARAMS_ATTCODE] = $sAttCode; + $aContextData[self::ENUM_PARAMS_ATTVALUE] = $oObject->Get($sAttCode); + + $oCurrentUser = UserRights::GetUserObject(); + if (false === is_null($oCurrentUser)) { + $aContextData[self::ENUM_PARAMS_USER] = get_class($oCurrentUser) . '::' . $oCurrentUser->GetKey(); + } + + parent::__construct('Attribute pointing to an object that is either non existing or not readable by the current user', $aContextData, '', $oPrevious); + } + + public function GetAttCode(): string + { + return $this->getContextData()[self::ENUM_PARAMS_ATTCODE]; + } + + public function GetAttValue(): string + { + return $this->getContextData()[self::ENUM_PARAMS_ATTVALUE]; + } +} diff --git a/application/utils.inc.php b/application/utils.inc.php index 6086f3874..8e4981093 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -2877,6 +2877,7 @@ HTML; * * @return bool if string null or empty * @since 3.0.2 N°5302 + * @since 2.7.10 N°6458 add method in the 2.7 branch */ public static function IsNullOrEmptyString(?string $sString): bool { @@ -2892,6 +2893,7 @@ HTML; * * @return bool if string is not null and not empty * @since 3.0.2 N°5302 + * @since 2.7.10 N°6458 add method in the 2.7 branch */ public static function IsNotNullOrEmptyString(?string $sString): bool { diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 1dc156895..2b912aaef 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2331,6 +2331,83 @@ abstract class DBObject implements iDisplay return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue); } + /** + * Checks for extkey attributes values. This will throw exception on non-existing as well as non-accessible objects (silo, scopes). + * That's why the test is done for all users including Administrators + * + * Note that due to perf issues, this isn't called directly by the ORM, but has to be called by consumers when possible. + * + * @param callable(string, string):bool|null $oIsObjectLoadableCallback Override to check if object is accessible. + * Parameters are object class and key + * Return value should be false if cannot access object, true otherwise + * @return void + * + * @throws ArchivedObjectException + * @throws CoreException if cannot get object attdef list + * @throws CoreUnexpectedValue + * @throws InvalidExternalKeyValueException + * @throws MySQLException + * @throws SecurityException if one extkey is pointing to an invalid value + * + * @link https://github.com/Combodo/iTop/security/advisories/GHSA-245j-66p9-pwmh + * @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6458 + * + * @see \RestUtils::FindObjectFromKey for the same check in the REST endpoint + */ + final public function CheckChangedExtKeysValues(callable $oIsObjectLoadableCallback = null) + { + if (is_null($oIsObjectLoadableCallback)) { + $oIsObjectLoadableCallback = function ($sClass, $sId) { + $oRemoteObject = MetaModel::GetObject($sClass, $sId, false); + if (is_null($oRemoteObject)) { + return false; + } + return true; + }; + } + + $aChanges = $this->ListChanges(); + $aAttCodesChanged = array_keys($aChanges); + foreach ($aAttCodesChanged as $sAttDefCode) { + $oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttDefCode); + + if ($oAttDef instanceof AttributeLinkedSetIndirect) { + /** @var ormLinkSet $oOrmSet */ + $oOrmSet = $this->Get($sAttDefCode); + while ($oLnk = $oOrmSet->Fetch()) { + $oLnk->CheckChangedExtKeysValues($oIsObjectLoadableCallback); + } + continue; + } + + /** @noinspection PhpConditionCheckedByNextConditionInspection */ + /** @noinspection NotOptimalIfConditionsInspection */ + if (($oAttDef instanceof AttributeHierarchicalKey) || ($oAttDef instanceof AttributeExternalKey)) { + $sRemoteObjectClass = $oAttDef->GetTargetClass(); + $sRemoteObjectKey = $this->Get($sAttDefCode); + } else if ($oAttDef instanceof AttributeObjectKey) { + $sRemoteObjectClassAttCode = $oAttDef->Get('class_attcode'); + $sRemoteObjectClass = $this->Get($sRemoteObjectClassAttCode); + $sRemoteObjectKey = $this->Get($sAttDefCode); + } else { + continue; + } + + /** @noinspection NotOptimalIfConditionsInspection */ + /** @noinspection TypeUnsafeComparisonInspection */ + if (utils::IsNullOrEmptyString($sRemoteObjectClass) + || utils::IsNullOrEmptyString($sRemoteObjectKey) + || ($sRemoteObjectKey == 0) // non-strict comparison as we might have bad surprises + ) { + continue; + } + + if (false === $oIsObjectLoadableCallback($sRemoteObjectClass, $sRemoteObjectKey)) { + throw new InvalidExternalKeyValueException($this, $sAttDefCode); + } + } + } + /** * Check if it is allowed to delete the existing object from the database * @@ -2476,11 +2553,11 @@ abstract class DBObject implements iDisplay * @api * @api-advanced * - * @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack - * @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation - * @return array attname => currentvalue List the attributes that have been changed using {@see DBObject::Set()}. + * @return array attcode => currentvalue List the attributes that have been changed using {@see DBObject::Set()}. * Reset during {@see DBObject::DBUpdate()} * @throws Exception + * @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack + * @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation * @uses m_aCurrValues */ public function ListChanges() @@ -2772,7 +2849,6 @@ abstract class DBObject implements iDisplay * @throws \Exception * * @internal - * */ public function DBInsertNoReload() { @@ -3056,8 +3132,6 @@ abstract class DBObject implements iDisplay * Persist an object to the DB, for the first time * * @api - * @see DBWrite - * * @return int|null inserted object key * * @throws \ArchivedObjectException @@ -3067,10 +3141,12 @@ abstract class DBObject implements iDisplay * @throws \CoreWarning * @throws \MySQLException * @throws \OQLException + * + * @see DBWrite */ public function DBInsert() { - $this->DBInsertNoReload(); + $this->DBInsertNoReload(); if (MetaModel::DBIsReadOnly()) { @@ -3129,13 +3205,13 @@ abstract class DBObject implements iDisplay * Update an object in DB * * @api - * @see DBObject::DBWrite() - * * @return int object key * * @throws \CoreException * @throws \CoreCannotSaveObjectException if CheckToWrite() returns issues * @throws \Exception + * + * @see DBObject::DBWrite() */ public function DBUpdate() { @@ -3143,6 +3219,7 @@ abstract class DBObject implements iDisplay { throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead"); } + // Protect against reentrance (e.g. cascading the update of ticket logs) static $aUpdateReentrance = array(); $sKey = get_class($this).'::'.$this->GetKey(); @@ -3519,13 +3596,18 @@ abstract class DBObject implements iDisplay /** * Make the current changes persistent - clever wrapper for Insert or Update - * - * @api + * + * @api * * @return int - * - * @throws \CoreCannotSaveObjectException - * @throws \CoreException + * + * @throws ArchivedObjectException + * @throws CoreCannotSaveObjectException + * @throws CoreException + * @throws CoreUnexpectedValue + * @throws CoreWarning + * @throws MySQLException + * @throws OQLException */ public function DBWrite() { diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 057c2ca5a..8291bb049 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -1430,8 +1430,10 @@ abstract class MetaModel * * @return AttributeDefinition[] * @throws \CoreException + * + * @see GetAttributesList for attcode list */ - final static public function ListAttributeDefs($sClass) + final public static function ListAttributeDefs($sClass) { self::_check_subclass($sClass); return self::$m_aAttribDefs[$sClass]; @@ -1444,8 +1446,10 @@ abstract class MetaModel * @param string[] $aDesiredAttTypes Array of AttributeDefinition classes to filter the list on * @param string|null $sListCode If provided, attributes will be limited to those in this zlist * - * @return array + * @return string[] list of attcodes * @throws \CoreException + * + * @see ListAttributeDefs to get AttributeDefinition array instead */ final public static function GetAttributesList(string $sClass, array $aDesiredAttTypes = [], ?string $sListCode = null) { diff --git a/core/userrights.class.inc.php b/core/userrights.class.inc.php index 15e58083b..c9693c4fa 100644 --- a/core/userrights.class.inc.php +++ b/core/userrights.class.inc.php @@ -1110,9 +1110,7 @@ class UserRights } /** - * Return the current user login or an empty string if nobody connected. - * - * @return string + * @return string connected {@see User} login field value, otherwise empty string */ public static function GetUser() { @@ -1560,9 +1558,9 @@ class UserRights /** * @param string $sClass - * @param int $iActionCode - * @param \DBObjectSet $oInstanceSet - * @param \User $oUser + * @param int $iActionCode see UR_ACTION_* constants + * @param DBObjectSet $oInstanceSet + * @param User $oUser * * @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS) * @throws \CoreException diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php index a092770a5..8e10bf7ed 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Form/ObjectFormManager.php @@ -31,6 +31,7 @@ use Combodo\iTop\Form\Field\LabelField; use Combodo\iTop\Form\Form; use Combodo\iTop\Form\FormManager; use Combodo\iTop\Portal\Helper\ApplicationHelper; +use Combodo\iTop\Portal\Helper\SecurityHelper; use CoreCannotSaveObjectException; use DBObject; use DBObjectSearch; @@ -42,6 +43,7 @@ use DOMXPath; use Exception; use ExceptionLog; use InlineImage; +use InvalidExternalKeyValueException; use IssueLog; use MetaModel; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -49,6 +51,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; use UserRights; use utils; +use const UR_ACTION_READ; /** * Description of ObjectFormManager @@ -1150,8 +1153,11 @@ class ObjectFormManager extends FormManager $bWasModified = $this->oObject->IsModified(); $bActivateTriggers = (!$bIsNew && $bWasModified); + /** @var SecurityHelper $oSecurityHelper */ + $oSecurityHelper = $this->oContainer->get('security_helper'); + // Forcing allowed writing on the object if necessary. This is used in some particular cases. - $bAllowWrite = $this->oContainer->get('security_helper')->IsActionAllowed($bIsNew ? UR_ACTION_CREATE : UR_ACTION_MODIFY, $sObjectClass, $this->oObject->GetKey()); + $bAllowWrite = $oSecurityHelper->IsActionAllowed($bIsNew ? UR_ACTION_CREATE : UR_ACTION_MODIFY, $sObjectClass, $this->oObject->GetKey()); if ($bAllowWrite) { $this->oObject->AllowWrite(true); } @@ -1159,12 +1165,15 @@ class ObjectFormManager extends FormManager // Writing object to DB try { + $this->oObject->CheckChangedExtKeysValues(function ($sClass, $sId) use ($oSecurityHelper): bool { + return $oSecurityHelper->IsActionAllowed(UR_ACTION_READ, $sClass, $sId); + }); $this->oObject->DBWrite(); - } - catch (CoreCannotSaveObjectException $e) { + } catch (CoreCannotSaveObjectException $e) { throw new Exception($e->getHtmlMessage()); - } - catch (Exception $e) { + } catch (InvalidExternalKeyValueException $e) { + throw new Exception($e->getIssue()); + } catch (Exception $e) { $aContext = [ 'origin' => __CLASS__.'::'.__METHOD__, 'obj_class' => get_class($this->oObject), diff --git a/lib/autoload.php b/lib/autoload.php index f37a5b80e..4daf9b579 100644 --- a/lib/autoload.php +++ b/lib/autoload.php @@ -2,6 +2,24 @@ // autoload.php @generated by Composer +if (PHP_VERSION_ID < 50600) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, $err); + } elseif (!headers_sent()) { + echo $err; + } + } + trigger_error( + $err, + E_USER_ERROR + ); +} + require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239::getLoader(); diff --git a/lib/composer/ClassLoader.php b/lib/composer/ClassLoader.php index 0cd6055d1..7824d8f7e 100644 --- a/lib/composer/ClassLoader.php +++ b/lib/composer/ClassLoader.php @@ -42,35 +42,37 @@ namespace Composer\Autoload; */ class ClassLoader { - /** @var ?string */ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ private $vendorDir; // PSR-4 /** - * @var array[] - * @psalm-var array> + * @var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array[] - * @psalm-var array> + * @var array> */ private $prefixDirsPsr4 = array(); /** - * @var array[] - * @psalm-var array + * @var list */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * @var array[] - * @psalm-var array> + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> */ private $prefixesPsr0 = array(); /** - * @var array[] - * @psalm-var array + * @var list */ private $fallbackDirsPsr0 = array(); @@ -78,8 +80,7 @@ class ClassLoader private $useIncludePath = false; /** - * @var string[] - * @psalm-var array + * @var array */ private $classMap = array(); @@ -87,29 +88,29 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var bool[] - * @psalm-var array + * @var array */ private $missingClasses = array(); - /** @var ?string */ + /** @var string|null */ private $apcuPrefix; /** - * @var self[] + * @var array */ private static $registeredLoaders = array(); /** - * @param ?string $vendorDir + * @param string|null $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); } /** - * @return string[] + * @return array> */ public function getPrefixes() { @@ -121,8 +122,7 @@ class ClassLoader } /** - * @return array[] - * @psalm-return array> + * @return array> */ public function getPrefixesPsr4() { @@ -130,8 +130,7 @@ class ClassLoader } /** - * @return array[] - * @psalm-return array + * @return list */ public function getFallbackDirs() { @@ -139,8 +138,7 @@ class ClassLoader } /** - * @return array[] - * @psalm-return array + * @return list */ public function getFallbackDirsPsr4() { @@ -148,8 +146,7 @@ class ClassLoader } /** - * @return string[] Array of classname => path - * @psalm-var array + * @return array Array of classname => path */ public function getClassMap() { @@ -157,8 +154,7 @@ class ClassLoader } /** - * @param string[] $classMap Class to filename map - * @psalm-param array $classMap + * @param array $classMap Class to filename map * * @return void */ @@ -175,24 +171,25 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - (array) $paths + $paths ); } @@ -201,19 +198,19 @@ class ClassLoader $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; + $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - (array) $paths + $paths ); } } @@ -222,9 +219,9 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -232,17 +229,18 @@ class ClassLoader */ public function addPsr4($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - (array) $paths + $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -252,18 +250,18 @@ class ClassLoader throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; + $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - (array) $paths + $paths ); } } @@ -272,8 +270,8 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories * * @return void */ @@ -290,8 +288,8 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -425,7 +423,8 @@ class ClassLoader public function loadClass($class) { if ($file = $this->findFile($class)) { - includeFile($file); + $includeFile = self::$includeFile; + $includeFile($file); return true; } @@ -476,9 +475,9 @@ class ClassLoader } /** - * Returns the currently registered loaders indexed by their corresponding vendor directories. + * Returns the currently registered loaders keyed by their corresponding vendor directories. * - * @return self[] + * @return array */ public static function getRegisteredLoaders() { @@ -555,18 +554,26 @@ class ClassLoader return false; } -} -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - * - * @param string $file - * @return void - * @private - */ -function includeFile($file) -{ - include $file; + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } } diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index acd509563..1666e694f 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -2,7 +2,7 @@ // autoload_classmap.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( @@ -662,6 +662,7 @@ return array( 'IntervalOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', 'Introspection' => $baseDir . '/core/introspection.class.inc.php', 'InvalidConfigParamException' => $baseDir . '/application/exceptions/InvalidConfigParamException.php', + 'InvalidExternalKeyValueException' => $baseDir . '/application/exceptions/InvalidExternalKeyValueException.php', 'InvalidPasswordAttributeOneWayPassword' => $baseDir . '/application/exceptions/InvalidPasswordAttributeOneWayPassword.php', 'IssueLog' => $baseDir . '/core/log.class.inc.php', 'ItopCounter' => $baseDir . '/core/counter.class.inc.php', diff --git a/lib/composer/autoload_files.php b/lib/composer/autoload_files.php index 7be757bea..e2cb88955 100644 --- a/lib/composer/autoload_files.php +++ b/lib/composer/autoload_files.php @@ -2,25 +2,25 @@ // autoload_files.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '7e9bd612cc444b3eed788ebbe46263a0' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php', '5255c38a0faeba867671b61dfda6d864' => $vendorDir . '/paragonie/random_compat/lib/random.php', '023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php', - '32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php', - '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', - 'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php', - '7e9bd612cc444b3eed788ebbe46263a0' => $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php', 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + 'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php', 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', 'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', + '32dcc8afd4335739640db7d200c1971d' => $vendorDir . '/symfony/polyfill-apcu/bootstrap.php', 'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php', + '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', '2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php', ); diff --git a/lib/composer/autoload_namespaces.php b/lib/composer/autoload_namespaces.php index d12922d08..e6117c750 100644 --- a/lib/composer/autoload_namespaces.php +++ b/lib/composer/autoload_namespaces.php @@ -2,7 +2,7 @@ // autoload_namespaces.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_psr4.php b/lib/composer/autoload_psr4.php index ca8b4b9f6..651c9f0c1 100644 --- a/lib/composer/autoload_psr4.php +++ b/lib/composer/autoload_psr4.php @@ -2,7 +2,7 @@ // autoload_psr4.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( diff --git a/lib/composer/autoload_real.php b/lib/composer/autoload_real.php index bc3e8fca0..9fb5fdc92 100644 --- a/lib/composer/autoload_real.php +++ b/lib/composer/autoload_real.php @@ -25,46 +25,31 @@ class ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239 require __DIR__ . '/platform_check.php'; spl_autoload_register(array('ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit5e7efdfe4e8f9526eb41991410b96239', 'loadClassLoader')); $includePaths = require __DIR__ . '/include_paths.php'; $includePaths[] = get_include_path(); set_include_path(implode(PATH_SEPARATOR, $includePaths)); - $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); - if ($useStaticLoader) { - require __DIR__ . '/autoload_static.php'; - - call_user_func(\Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::getInitializer($loader)); - } else { - $classMap = require __DIR__ . '/autoload_classmap.php'; - if ($classMap) { - $loader->addClassMap($classMap); - } - } + require __DIR__ . '/autoload_static.php'; + call_user_func(\Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::getInitializer($loader)); $loader->setClassMapAuthoritative(true); $loader->register(true); - if ($useStaticLoader) { - $includeFiles = Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::$files; - } else { - $includeFiles = require __DIR__ . '/autoload_files.php'; - } - foreach ($includeFiles as $fileIdentifier => $file) { - composerRequire5e7efdfe4e8f9526eb41991410b96239($fileIdentifier, $file); + $filesToLoad = \Composer\Autoload\ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); } return $loader; } } - -function composerRequire5e7efdfe4e8f9526eb41991410b96239($fileIdentifier, $file) -{ - if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - require $file; - - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - } -} diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 6fc0575bf..664bbc8c9 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -9,20 +9,20 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239 public static $files = array ( '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '7e9bd612cc444b3eed788ebbe46263a0' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php', '5255c38a0faeba867671b61dfda6d864' => __DIR__ . '/..' . '/paragonie/random_compat/lib/random.php', '023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php', - '32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php', - '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', - 'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php', - '7e9bd612cc444b3eed788ebbe46263a0' => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php', 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + 'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php', 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', 'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + '32dcc8afd4335739640db7d200c1971d' => __DIR__ . '/..' . '/symfony/polyfill-apcu/bootstrap.php', 'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php', + '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', '2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php', ); @@ -1030,6 +1030,7 @@ class ComposerStaticInit5e7efdfe4e8f9526eb41991410b96239 'IntervalOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', 'Introspection' => __DIR__ . '/../..' . '/core/introspection.class.inc.php', 'InvalidConfigParamException' => __DIR__ . '/../..' . '/application/exceptions/InvalidConfigParamException.php', + 'InvalidExternalKeyValueException' => __DIR__ . '/../..' . '/application/exceptions/InvalidExternalKeyValueException.php', 'InvalidPasswordAttributeOneWayPassword' => __DIR__ . '/../..' . '/application/exceptions/InvalidPasswordAttributeOneWayPassword.php', 'IssueLog' => __DIR__ . '/../..' . '/core/log.class.inc.php', 'ItopCounter' => __DIR__ . '/../..' . '/core/counter.class.inc.php', diff --git a/lib/composer/include_paths.php b/lib/composer/include_paths.php index d4fb96718..af33c1491 100644 --- a/lib/composer/include_paths.php +++ b/lib/composer/include_paths.php @@ -2,7 +2,7 @@ // include_paths.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( diff --git a/pages/UI.php b/pages/UI.php index f8ac61627..eb6941fa9 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -870,6 +870,7 @@ EOF { throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); } + $oObj->CheckChangedExtKeysValues(); // Transactions are now handled in DBUpdate $oObj->DBUpdate(); $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); @@ -1108,6 +1109,7 @@ EOF throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); } + $oObj->CheckChangedExtKeysValues(); $oObj->DBInsertNoReload();// No need to reload IssueLog::Trace('Object created', $sClass, array( diff --git a/tests/php-unit-tests/README.md b/tests/php-unit-tests/README.md index 0b5d06b3d..3f556426f 100644 --- a/tests/php-unit-tests/README.md +++ b/tests/php-unit-tests/README.md @@ -59,6 +59,16 @@ Fix that in the XML configuration in the PHP section ``` + +### Measure the time spent in a test + +Simply cut'n paste the following line at several places within the test function: + +```php +if (isset($fStarted)) {echo 'L'.__LINE__.': '.round(microtime(true) - $fStarted, 3)."\n";} $fStarted = microtime(true); +``` + + ### Understand tests interactions With PHPStorm, select two tests, right click to get the context menu, then `run`. @@ -119,4 +129,3 @@ This won't work because the comment MUST start with `/**` (two stars) to be cons Therefore, if the tests are isolated, then `setupBeforeClass` will be called as often as `setUp`. -This has been proven with [`runClassInSeparateProcessTest.php`](experiments/runClassInSeparateProcessTest.php) \ No newline at end of file diff --git a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php index a1f86d44f..1d9555813 100644 --- a/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php +++ b/tests/php-unit-tests/src/BaseTestCase/ItopDataTestCase.php @@ -709,7 +709,7 @@ abstract class ItopDataTestCase extends ItopTestCase * @return array * @throws Exception */ - protected function AddCIToTicket($oCI, $oTicket, $sImpactCode) + protected function AddCIToTicket($oCI, $oTicket, $sImpactCode = 'manual') { $oNewLink = new lnkFunctionalCIToTicket(); $oNewLink->Set('functionalci_id', $oCI->GetKey()); diff --git a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php index a63758fff..32ac9fc85 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBObjectTest.php @@ -17,18 +17,20 @@ // along with iTop. If not, see // -/** - * Created by PhpStorm. - * User: Eric - * Date: 02/10/2017 - * Time: 13:58 - */ - namespace Combodo\iTop\Test\UnitTest\Core; +use Attachment; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use DBObject; +use InvalidExternalKeyValueException; +use lnkPersonToTeam; use MetaModel; +use Organization; +use Person; +use Team; +use User; +use UserRights; +use utils; /** @@ -37,6 +39,7 @@ use MetaModel; class DBObjectTest extends ItopDataTestCase { const CREATE_TEST_ORG = true; + const INVALID_OBJECT_KEY = 123456789; protected function setUp(): void { @@ -280,4 +283,265 @@ class DBObjectTest extends ItopDataTestCase } } } + + private function GetAlwaysTrueCallback(): callable + { + return static function () { + return true; + }; + } + + private function GetAlwaysFalseCallback(): callable + { + return static function () { + return false; + }; + } + + /** + * @covers DBObject::CheckChangedExtKeysValues() + * @runInSeparateProcess MetaModel::GetObject returning wrong values :( + */ + public function testCheckExtKeysSiloOnAttributeExternalKey() + { + //--- Preparing data... + $oAlwaysTrueCallback = $this->GetAlwaysTrueCallback(); + $oAlwaysFalseCallback = $this->GetAlwaysFalseCallback(); + + /** @var Organization $oDemoOrg */ + $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo'); + /** @var Organization $oMyCompanyOrg */ + $oMyCompanyOrg = MetaModel::GetObjectByName(Organization::class, 'My Company/Department'); + + /** @var Person $oPersonOfDemoOrg */ + $oPersonOfDemoOrg = MetaModel::GetObjectByName(Person::class, 'Agatha Christie'); + /** @var Person $oPersonOfMyCompanyOrg */ + $oPersonOfMyCompanyOrg = MetaModel::GetObjectByName(Person::class, 'My first name My last name'); + + $sConfigurationManagerProfileId = 3; // Access to Person objects + $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigurationManagerProfileId); + + $oAdminUser = MetaModel::GetObjectByName(User::class, 'admin', false); + if (is_null($oAdminUser)) { + $oAdminUser = $this->CreateUser('admin', 1); + } + + /** @var Person $oPersonObject */ + $oPersonObject = $this->CreatePerson(0, $oMyCompanyOrg->GetKey()); + + //--- Now we can do some tests ! + UserRights::Login($oUserWithAllowedOrgs->Get('login')); + $this->ResetMetaModelQueyCacheGetObject(); + + try { + $oPersonObject->CheckChangedExtKeysValues(); + } catch (InvalidExternalKeyValueException $eCannotSave) { + $this->fail('Should skip external keys already written in Database'); + } + + $oPersonObject->Set('manager_id', $oPersonOfDemoOrg->GetKey()); + try { + $oPersonObject->CheckChangedExtKeysValues(); + } catch (InvalidExternalKeyValueException $eCannotSave) { + $this->fail('Should allow objects in the same org as the current user'); + } + + try { + $oPersonObject->CheckChangedExtKeysValues($oAlwaysFalseCallback); + $this->fail('Should consider the callback returning "false"'); + } catch (InvalidExternalKeyValueException $eCannotSave) { + // Ok, the exception was expected + } + + $oPersonObject->Set('manager_id', $oPersonOfMyCompanyOrg->GetKey()); + try { + $oPersonObject->CheckChangedExtKeysValues(); + $this->fail('Should not allow objects not being in the allowed orgs of the current user'); + } catch (InvalidExternalKeyValueException $eCannotSave) { + $this->assertEquals('manager_id', $eCannotSave->GetAttCode(), 'Should report the wrong external key attcode'); + $this->assertEquals($oMyCompanyOrg->GetKey(), $eCannotSave->GetAttValue(), 'Should report the unauthorized external key value'); + } + + try { + $oPersonObject->CheckChangedExtKeysValues($oAlwaysTrueCallback); + } catch (InvalidExternalKeyValueException $eCannotSave) { + $this->fail('Should consider the callback returning "true"'); + } + + UserRights::Logoff(); + $this->ResetMetaModelQueyCacheGetObject(); + + UserRights::Login($oAdminUser->Get('login')); + $oPersonObject->CheckChangedExtKeysValues(); + $this->assertTrue(true, 'Admin user can create objects in any org'); + } + + /** + * @covers DBObject::CheckChangedExtKeysValues() + * @runInSeparateProcess MetaModel::GetObject returning wrong values :( + */ + public function testCheckExtKeysOnAttributeLinkedSetIndirect() + { + //--- Preparing data... + /** @var Organization $oDemoOrg */ + $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo'); + /** @var Person $oPersonOnItDepartmentOrg */ + $oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda'); + /** @var Person $oPersonOnDemoOrg */ + $oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet'); + + $sConfigManagerProfileId = 3; // access to Team and Contact objects + $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId); + + //--- Now we can do some tests ! + UserRights::Login($oUserWithAllowedOrgs->Get('login')); + $this->ResetMetaModelQueyCacheGetObject(); + + $oTeam = MetaModel::NewObject(Team::class, [ + 'name' => 'The A Team', + 'org_id' => $oDemoOrg->GetKey() + ]); + + // Part 1 - Test with an invalid id (non-existing object) + // + $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class); + $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [ + 'person_id' => self::INVALID_OBJECT_KEY, + ])); + $oTeam->Set('persons_list', $oPersonLinks); + + try { + $oTeam->CheckChangedExtKeysValues(); + $this->fail('An unknown object should be detected as invalid'); + } catch (InvalidExternalKeyValueException $e) { + // we are getting the exception on the lnk class + // In consequence attcode is `lnkPersonToTeam.person_id` instead of `Team.persons_list` + $this->assertEquals('person_id', $e->GetAttCode(), 'The reported attcode should be the external key on the link'); + $this->assertEquals(self::INVALID_OBJECT_KEY, $e->GetAttValue(), 'The reported value should be the external key on the link'); + } + + try { + $oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback()); + } catch (InvalidExternalKeyValueException $e) { + $this->fail('Should have no error when callback returns true'); + } + + // Part 2 - Test with an allowed object + // + $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class); + $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [ + 'person_id' => $oPersonOnDemoOrg->GetKey(), + ])); + $oTeam->Set('persons_list', $oPersonLinks); + + try { + $oTeam->CheckChangedExtKeysValues(); + } catch (InvalidExternalKeyValueException $e) { + $this->fail('An authorized object should be detected as valid'); + } + + try { + $oTeam->CheckChangedExtKeysValues($this->GetAlwaysFalseCallback()); + $this->fail('Should cascade the callback result when it is "false"'); + } catch (InvalidExternalKeyValueException $e) { + // Ok, the exception was expected + } + + // Part 3 - Test with a not allowed object + // + $oPersonLinks = \DBObjectSet::FromScratch(lnkPersonToTeam::class); + $oPersonLinks->AddObject(MetaModel::NewObject(lnkPersonToTeam::class, [ + 'person_id' => $oPersonOnItDepartmentOrg->GetKey(), + ])); + $oTeam->Set('persons_list', $oPersonLinks); + + try { + $oTeam->CheckChangedExtKeysValues(); + $this->fail('An unauthorized object should be detected as invalid'); + } + catch (InvalidExternalKeyValueException $e) { + // Ok, the exception was expected + } + + try { + $oTeam->CheckChangedExtKeysValues($this->GetAlwaysTrueCallback()); + } catch (InvalidExternalKeyValueException $e) { + $this->fail('Should cascade the callback result when it is "true"'); + } + + $oTeam->DBInsert(); // persisting invalid value and resets the object changed values + try { + $oTeam->CheckChangedExtKeysValues(); + } + catch (InvalidExternalKeyValueException $e) { + $this->fail('An unauthorized value should be ignored when it is not being modified'); + } + } + + /** + * @covers DBObject::CheckChangedExtKeysValues() + * @runInSeparateProcess MetaModel::GetObject returning wrong values :( + */ + public function testCheckExtKeysSiloOnAttributeObjectKey() + { + //--- Preparing data... + /** @var Organization $oDemoOrg */ + $oDemoOrg = MetaModel::GetObjectByName(Organization::class, 'Demo'); + /** @var Person $oPersonOnItDepartmentOrg */ + $oPersonOnItDepartmentOrg = MetaModel::GetObjectByName(Person::class, 'Anna Gavalda'); + /** @var Person $oPersonOnDemoOrg */ + $oPersonOnDemoOrg = MetaModel::GetObjectByName(Person::class, 'Claude Monet'); + + $sConfigManagerProfileId = 3; // access to Team and Contact objects + $oUserWithAllowedOrgs = $this->CreateDemoOrgUser($oDemoOrg, $sConfigManagerProfileId); + + //--- Now we can do some tests ! + UserRights::Login($oUserWithAllowedOrgs->Get('login')); + $this->ResetMetaModelQueyCacheGetObject(); + + $oAttachment = MetaModel::NewObject(Attachment::class, [ + 'item_class' => Person::class, + 'item_id' => $oPersonOnDemoOrg->GetKey(), + ]); + try { + $oAttachment->CheckChangedExtKeysValues(); + } catch (InvalidExternalKeyValueException $e) { + $this->fail('Should be allowed to create an attachment pointing to a ticket in the allowed org list'); + } + + $oAttachment = MetaModel::NewObject(Attachment::class, [ + 'item_class' => Person::class, + 'item_id' => $oPersonOnItDepartmentOrg->GetKey(), + ]); + $this->ResetMetaModelQueyCacheGetObject(); + try { + $oAttachment->CheckChangedExtKeysValues(); + $this->fail('There should be an error on attachment pointing to a non allowed org object'); + } catch (InvalidExternalKeyValueException $e) { + $this->assertEquals('item_id', $e->GetAttCode(), 'Should report the object key attribute'); + $this->assertEquals($oPersonOnItDepartmentOrg->GetKey(), $e->GetAttValue(), 'Should report the object key value'); + } + } + + /** + * Helper to reset the metamodel cache + * We might need to create something generic and add it to {@see UserRights::Logoff()} ? + */ + private function ResetMetaModelQueyCacheGetObject() { + $this->SetNonPublicStaticProperty(MetaModel::class, 'aQueryCacheGetObject', []); + } + + private function CreateDemoOrgUser(Organization $oDemoOrg, string $sProfileId): User + { + utils::GetConfig()->SetModuleSetting('authent-local', 'password_validation.pattern', ''); + $oUserWithAllowedOrgs = $this->CreateContactlessUser('demo_test_' . __CLASS__, $sProfileId); + /** @var \URP_UserOrg $oUserOrg */ + $oUserOrg = \MetaModel::NewObject('URP_UserOrg', ['allowed_org_id' => $oDemoOrg->GetKey(),]); + $oAllowedOrgList = $oUserWithAllowedOrgs->Get('allowed_org_list'); + $oAllowedOrgList->AddItem($oUserOrg); + $oUserWithAllowedOrgs->Set('allowed_org_list', $oAllowedOrgList); + $oUserWithAllowedOrgs->DBWrite(); + + return $oUserWithAllowedOrgs; + } }